From 137635399e7fe94004e1de505ed0d02e08c8f95c Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Sat, 31 Jul 2021 03:47:24 +0200 Subject: [PATCH 01/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20REFACTOR:=20Split=20?= =?UTF-8?q?`basic`=20executor=20->=20`local-serial`=20&=20`temp-serial`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yml | 6 +- jupyter_cache/cli/arguments.py | 2 + jupyter_cache/cli/commands/cmd_exec.py | 32 ++++-- jupyter_cache/cli/options.py | 5 +- jupyter_cache/executors/base.py | 45 +++++++-- jupyter_cache/executors/basic.py | 134 +++++++++++++++---------- setup.cfg | 3 +- tests/test_cache.py | 57 ++++++----- tox.ini | 5 + 9 files changed, 191 insertions(+), 98 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 77f488f..af17ff8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9] include: - os: windows-latest python-version: 3.7 @@ -67,10 +67,10 @@ jobs: steps: - name: Checkout source uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.8 - name: Build package run: | pip install wheel diff --git a/jupyter_cache/cli/arguments.py b/jupyter_cache/cli/arguments.py index 12ef22f..6aa3d48 100644 --- a/jupyter_cache/cli/arguments.py +++ b/jupyter_cache/cli/arguments.py @@ -34,3 +34,5 @@ PK = click.argument("pk", metavar="ID", type=int) PKS = click.argument("pks", metavar="IDs", nargs=-1, type=int) + +PK_OR_PATHS = click.argument("pk_paths", metavar="ID_OR_PATHS", nargs=-1) diff --git a/jupyter_cache/cli/commands/cmd_exec.py b/jupyter_cache/cli/commands/cmd_exec.py index bae852e..ad03a83 100644 --- a/jupyter_cache/cli/commands/cmd_exec.py +++ b/jupyter_cache/cli/commands/cmd_exec.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path import click import click_log @@ -16,21 +17,40 @@ @options.EXEC_ENTRYPOINT @options.EXEC_TIMEOUT @options.CACHE_PATH -@arguments.PKS -def execute_nbs(cache_path, entry_point, pks, timeout): - """Execute staged notebooks that are outdated.""" +@arguments.PK_OR_PATHS +def execute_nbs(cache_path, entry_point, pk_paths, timeout): + """Execute notebooks that are not in the cache or outdated.""" import yaml from jupyter_cache.executors import load_executor db = get_cache(cache_path) + records = [] + for pk_path in pk_paths: + if pk_path.isdigit(): + pk_path = int(pk_path) + record = db.get_staged_record(int(pk_path)) + else: + try: + record = db.get_staged_record(pk_path) + except KeyError: + if not Path(pk_path).exists(): + raise FileNotFoundError(f"'{pk_path}' does not exist.") + record = db.stage_notebook_file(pk_path) + records.append(record) try: - executor = load_executor("basic", db, logger=logger) + executor = load_executor(entry_point, db, logger=logger) except ImportError as error: logger.error(str(error)) return 1 - result = executor.run_and_cache(filter_pks=pks or None, timeout=timeout) + result = executor.run_and_cache( + filter_pks=[record.pk for record in records] or None, timeout=timeout + ) click.secho( "Finished! Successfully executed notebooks have been cached.", fg="green" ) - click.echo(yaml.safe_dump(result, sort_keys=False)) + output = result.as_json() + output["up-to-date"] = list( + {record.uri for record in records}.difference(result.all()) + ) + click.echo(yaml.safe_dump(output, sort_keys=False)) diff --git a/jupyter_cache/cli/options.py b/jupyter_cache/cli/options.py index c33998a..eae0d89 100644 --- a/jupyter_cache/cli/options.py +++ b/jupyter_cache/cli/options.py @@ -76,8 +76,9 @@ def check_cache_exists(ctx, param, value): EXEC_ENTRYPOINT = click.option( "-e", "--entry-point", - help="The entry-point from which to load the executor.", - default="basic", + # TODO list additional entry points + help="The entry-point from which to load the executor [local-serial|temp-serial].", + default="local-serial", show_default=True, ) diff --git a/jupyter_cache/executors/base.py b/jupyter_cache/executors/base.py index 5987206..a81597e 100644 --- a/jupyter_cache/executors/base.py +++ b/jupyter_cache/executors/base.py @@ -1,14 +1,14 @@ import logging from abc import ABC, abstractmethod -from typing import Callable, List, Optional +from typing import Any, Callable, Dict, List, Optional +import attr + +# TODO use importlib.metadata import pkg_resources from jupyter_cache.base import JupyterCacheAbstract -# TODO abstact -from jupyter_cache.cache.db import NbCacheRecord - ENTRY_POINT_GROUP = "jupyter_executors" base_logger = logging.getLogger(__name__) @@ -18,6 +18,30 @@ class ExecutionError(Exception): pass +@attr.s(slots=True) +class ExecutorRunResult: + """A container for the execution result.""" + + # URIs of notebooks which where successfully executed + succeeded: List[str] = attr.ib(factory=list) + # URIs of notebooks which excepted during execution + excepted: List[str] = attr.ib(factory=list) + # URIs of notebooks which errored before execution + errored: List[str] = attr.ib(factory=list) + + def all(self) -> List[str]: + """Return all notebooks.""" + return self.succeeded + self.excepted + self.errored + + def as_json(self) -> Dict[str, Any]: + """Return the result as a JSON serializable dict.""" + return { + "succeeded": self.succeeded, + "excepted": self.excepted, + "errored": self.errored, + } + + class JupyterExecutorAbstract(ABC): """An abstract class for executing notebooks in a cache.""" @@ -39,11 +63,14 @@ def logger(self): @abstractmethod def run_and_cache( self, + *, filter_uris: Optional[List[str]] = None, filter_pks: Optional[List[int]] = None, converter: Optional[Callable] = None, - **kwargs - ) -> List[NbCacheRecord]: + timeout: Optional[int] = 30, + allow_errors: bool = False, + **kwargs: Any + ) -> ExecutorRunResult: """Run execution, cache successfully executed notebooks and return their URIs Parameters @@ -55,8 +82,12 @@ def run_and_cache( converter: An optional converter for staged notebooks, which takes the URI and returns a notebook node + timeout: int + Maximum time in seconds to wait for a single cell to run for + allow_errors: bool + Whether to halt execution on the first cell exception + (provided the cell is not tagged as an expected exception) """ - pass def list_executors(): diff --git a/jupyter_cache/executors/basic.py b/jupyter_cache/executors/basic.py index e7e8d0f..2216b91 100644 --- a/jupyter_cache/executors/basic.py +++ b/jupyter_cache/executors/basic.py @@ -1,10 +1,11 @@ import shutil import tempfile from pathlib import Path +from typing import Optional from jupyter_cache.cache.db import NbStageRecord from jupyter_cache.cache.main import NbArtifacts, NbBundleIn -from jupyter_cache.executors.base import JupyterExecutorAbstract +from jupyter_cache.executors.base import ExecutorRunResult, JupyterExecutorAbstract from jupyter_cache.executors.utils import single_nb_execution from jupyter_cache.utils import to_relative_paths @@ -20,8 +21,8 @@ def __init__(self, message, uri, exc): return super().__init__(message) -class JupyterExecutorBasic(JupyterExecutorAbstract): - """A basic implementation of an executor. +class JupyterExecutorLocalSerial(JupyterExecutorAbstract): + """A basic implementation of an executor; executing locally in serial. The execution is split into two methods: `run` and `execute`. In this way access to the cache can be synchronous, but the execution can be @@ -30,13 +31,13 @@ class JupyterExecutorBasic(JupyterExecutorAbstract): def run_and_cache( self, + *, filter_uris=None, filter_pks=None, converter=None, timeout=30, allow_errors=False, - run_in_temp=True, - ): + ) -> ExecutorRunResult: """This function interfaces with the cache, deferring execution to `execute`.""" # Get the notebook tha require re-execution stage_records = self.cache.list_staged_unexecuted(converter=converter) @@ -51,7 +52,7 @@ def run_and_cache( # setup an dictionary to categorise all executed notebook uris: # excepted are where the actual notebook execution raised an exception; # errored is where any other exception was raised - result = {"succeeded": [], "excepted": [], "errored": []} + result = ExecutorRunResult() # we pass an iterator to the execute method, # so that we don't have to read all notebooks before execution @@ -65,22 +66,24 @@ def _iterator(): self.logger.error( "Failed Retrieving: {}".format(stage_record.uri), exc_info=True ) - result["errored"].append(stage_record.uri) + result.errored.append(stage_record.uri) else: yield stage_record, nb_bundle # The execute method yields notebook bundles, or ExecutionError for bundle_or_exc in self.execute( - _iterator(), int(timeout), allow_errors, run_in_temp + _iterator(), + int(timeout), + allow_errors, ): if isinstance(bundle_or_exc, ExecutionError): self.logger.error(bundle_or_exc.uri, exc_info=bundle_or_exc.exc) - result["errored"].append(bundle_or_exc.uri) + result.errored.append(bundle_or_exc.uri) continue elif bundle_or_exc.traceback is not None: # The notebook raised an exception during execution # TODO store excepted bundles - result["excepted"].append(bundle_or_exc.uri) + result.excepted.append(bundle_or_exc.uri) NbStageRecord.set_traceback( bundle_or_exc.uri, bundle_or_exc.traceback, self.cache.db ) @@ -94,9 +97,9 @@ def _iterator(): self.logger.error( "Failed Caching: {}".format(bundle_or_exc.uri), exc_info=True ) - result["errored"].append(bundle_or_exc.uri) + result.errored.append(bundle_or_exc.uri) else: - result["succeeded"].append(bundle_or_exc.uri) + result.succeeded.append(bundle_or_exc.uri) # TODO it would also be ideal to tag all notebooks # that were executed at the same time (just part of `data` or separate column?). @@ -109,7 +112,62 @@ def _iterator(): return result - def execute(self, input_iterator, timeout=30, allow_errors=False, in_temp=True): + def execute_single( + self, + nb_bundle, + uri: str, + cwd: Optional[str], + timeout: Optional[int], + allow_errors: bool, + asset_files, + ): + result = single_nb_execution( + nb_bundle.nb, + cwd=cwd, + timeout=timeout, + allow_errors=allow_errors, + ) + if result.err: + self.logger.error("Execution Failed: {}".format(uri)) + return _create_bundle( + nb_bundle, + cwd, + asset_files, + result.time, + result.exc_string, + ) + + self.logger.info("Execution Succeeded: {}".format(uri)) + return _create_bundle(nb_bundle, cwd, asset_files, result.time, None) + + def execute(self, input_iterator, timeout=30, allow_errors=False): + """This function is isolated from the cache, and is responsible for execution. + + The method is only supplied with the staged record and input notebook bundle, + it then yield results for caching + """ + for _, nb_bundle in input_iterator: + try: + uri = nb_bundle.uri + self.logger.info("Executing: {}".format(uri)) + + yield self.execute_single( + nb_bundle, + uri, + str(Path(uri).parent), + timeout, + allow_errors, + None, + ) + + except Exception as err: + yield ExecutionError("Unexpected Error", uri, err) + + +class JupyterExecutorTempSerial(JupyterExecutorLocalSerial): + """An implementation of an executor; executing in a temporary folder in serial.""" + + def execute(self, input_iterator, timeout=30, allow_errors=False): """This function is isolated from the cache, and is responsible for execution. The method is only supplied with the staged record and input notebook bundle, @@ -120,56 +178,26 @@ def execute(self, input_iterator, timeout=30, allow_errors=False, in_temp=True): uri = nb_bundle.uri self.logger.info("Executing: {}".format(uri)) - if in_temp: - with tempfile.TemporaryDirectory() as tmpdirname: - - try: - asset_files = _copy_assets(stage_record, tmpdirname) - except Exception as err: - yield ExecutionError("Assets Retrieval Error", uri, err) - continue - - yield self.execute_single( - nb_bundle, - uri, - tmpdirname, - timeout, - allow_errors, - asset_files, - ) - else: + with tempfile.TemporaryDirectory() as tmpdirname: + + try: + asset_files = _copy_assets(stage_record, tmpdirname) + except Exception as err: + yield ExecutionError("Assets Retrieval Error", uri, err) + continue + yield self.execute_single( nb_bundle, uri, - str(Path(uri).parent), + tmpdirname, timeout, allow_errors, - None, + asset_files, ) except Exception as err: yield ExecutionError("Unexpected Error", uri, err) - def execute_single(self, nb_bundle, uri, cwd, timeout, allow_errors, asset_files): - result = single_nb_execution( - nb_bundle.nb, - cwd=cwd, - timeout=timeout, - allow_errors=allow_errors, - ) - if result.err: - self.logger.error("Execution Failed: {}".format(uri)) - return _create_bundle( - nb_bundle, - cwd, - asset_files, - result.time, - result.exc_string, - ) - - self.logger.info("Execution Succeeded: {}".format(uri)) - return _create_bundle(nb_bundle, cwd, asset_files, result.time, None) - def _copy_assets(record, folder): """Copy notebook assets to the folder the notebook will be executed in.""" diff --git a/setup.cfg b/setup.cfg index 6a30278..8ba61ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,7 +41,8 @@ zip_safe = True console_scripts = jcache = jupyter_cache.cli.commands.cmd_main:jcache jupyter_executors = - basic = jupyter_cache.executors.basic:JupyterExecutorBasic + local-serial = jupyter_cache.executors.basic:JupyterExecutorLocalSerial + temp-serial = jupyter_cache.executors.basic:JupyterExecutorTempSerial [options.extras_require] cli = diff --git a/tests/test_cache.py b/tests/test_cache.py index ae871ee..e6163e7 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,4 +1,5 @@ import os +import shutil from textwrap import dedent import nbformat as nbf @@ -184,25 +185,28 @@ def test_artifacts(tmp_path): # jupyter_client/session.py:371: DeprecationWarning: # Session._key_changed is deprecated in traitlets: use @observe and @unobserve instead @pytest.mark.filterwarnings("ignore") -def test_execution(tmp_path): +@pytest.mark.parametrize("executor_key", ["local-serial", "temp-serial"]) +def test_execution(tmp_path, executor_key): from jupyter_cache.executors import load_executor - db = JupyterCacheBase(str(tmp_path)) - db.stage_notebook_file(path=os.path.join(NB_PATH, "basic_unrun.ipynb")) - db.stage_notebook_file(path=os.path.join(NB_PATH, "basic_failing.ipynb")) + db = JupyterCacheBase(str(tmp_path / "cache")) + temp_nb_path = tmp_path / "notebooks" + shutil.copytree(NB_PATH, temp_nb_path) + db.stage_notebook_file(path=os.path.join(temp_nb_path, "basic_unrun.ipynb")) + db.stage_notebook_file(path=os.path.join(temp_nb_path, "basic_failing.ipynb")) db.stage_notebook_file( - path=os.path.join(NB_PATH, "external_output.ipynb"), - assets=(os.path.join(NB_PATH, "basic.ipynb"),), + path=os.path.join(temp_nb_path, "external_output.ipynb"), + assets=(os.path.join(temp_nb_path, "basic.ipynb"),), ) - executor = load_executor("basic", db) + executor = load_executor(executor_key, db) result = executor.run_and_cache() print(result) - assert result == { + assert result.as_json() == { "succeeded": [ - os.path.join(NB_PATH, "basic_unrun.ipynb"), - os.path.join(NB_PATH, "external_output.ipynb"), + os.path.join(temp_nb_path, "basic_unrun.ipynb"), + os.path.join(temp_nb_path, "external_output.ipynb"), ], - "excepted": [os.path.join(NB_PATH, "basic_failing.ipynb")], + "excepted": [os.path.join(temp_nb_path, "basic_failing.ipynb")], "errored": [], } assert len(db.list_cache_records()) == 2 @@ -215,10 +219,11 @@ def test_execution(tmp_path): "source": "a=1\nprint(a)", } assert "execution_seconds" in bundle.record.data - with db.cache_artefacts_temppath(2) as path: - paths = [str(p.relative_to(path)) for p in path.glob("**/*") if p.is_file()] - assert paths == ["artifact.txt"] - assert path.joinpath("artifact.txt").read_text(encoding="utf8") == "hi" + if "temp" in executor_key: + with db.cache_artefacts_temppath(2) as path: + paths = [str(p.relative_to(path)) for p in path.glob("**/*") if p.is_file()] + assert paths == ["artifact.txt"] + assert path.joinpath("artifact.txt").read_text(encoding="utf8") == "hi" stage_record = db.get_staged_record(2) assert stage_record.traceback is not None assert "Exception: oopsie!" in stage_record.traceback @@ -231,9 +236,9 @@ def test_execution_timeout_config(tmp_path): db = JupyterCacheBase(str(tmp_path)) db.stage_notebook_file(path=os.path.join(NB_PATH, "sleep_2.ipynb")) - executor = load_executor("basic", db) + executor = load_executor("local-serial", db) result = executor.run_and_cache(timeout=10) - assert result == { + assert result.as_json() == { "succeeded": [os.path.join(NB_PATH, "sleep_2.ipynb")], "excepted": [], "errored": [], @@ -241,9 +246,9 @@ def test_execution_timeout_config(tmp_path): db.clear_cache() db.stage_notebook_file(path=os.path.join(NB_PATH, "sleep_2.ipynb")) - executor = load_executor("basic", db) + executor = load_executor("local-serial", db) result = executor.run_and_cache(timeout=1) - assert result == { + assert result.as_json() == { "succeeded": [], "excepted": [os.path.join(NB_PATH, "sleep_2.ipynb")], "errored": [], @@ -257,9 +262,9 @@ def test_execution_timeout_metadata(tmp_path): db = JupyterCacheBase(str(tmp_path)) db.stage_notebook_file(path=os.path.join(NB_PATH, "sleep_2_timeout_1.ipynb")) - executor = load_executor("basic", db) + executor = load_executor("local-serial", db) result = executor.run_and_cache() - assert result == { + assert result.as_json() == { "succeeded": [], "excepted": [os.path.join(NB_PATH, "sleep_2_timeout_1.ipynb")], "errored": [], @@ -272,9 +277,9 @@ def test_execution_allow_errors_config(tmp_path): db = JupyterCacheBase(str(tmp_path)) db.stage_notebook_file(path=os.path.join(NB_PATH, "basic_failing.ipynb")) - executor = load_executor("basic", db) + executor = load_executor("local-serial", db) result = executor.run_and_cache(allow_errors=True) - assert result == { + assert result.as_json() == { "succeeded": [os.path.join(NB_PATH, "basic_failing.ipynb")], "excepted": [], "errored": [], @@ -287,9 +292,9 @@ def test_run_in_temp_false(tmp_path): db = JupyterCacheBase(str(tmp_path)) db.stage_notebook_file(path=os.path.join(NB_PATH, "basic.ipynb")) - executor = load_executor("basic", db) - result = executor.run_and_cache(run_in_temp=False) - assert result == { + executor = load_executor("temp-serial", db) + result = executor.run_and_cache() + assert result.as_json() == { "succeeded": [os.path.join(NB_PATH, "basic.ipynb")], "excepted": [], "errored": [], diff --git a/tox.ini b/tox.ini index c975f2d..a6faccb 100644 --- a/tox.ini +++ b/tox.ini @@ -18,10 +18,15 @@ usedevelop = true [testenv:py{36,37,38,39}] extras = cli,testing +deps = + black + flake8 commands = pytest {posargs} [testenv:cli] extras = cli +deps = + ipykernel commands = jcache {posargs} [testenv:docs-{clean,update}] From 51973180a2d7ae88e53e86f4e5fc7f32a87ffe91 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Sat, 31 Jul 2021 03:58:31 +0200 Subject: [PATCH 02/39] =?UTF-8?q?=F0=9F=94=A7=20MAINTAIN:=20remove=20unnec?= =?UTF-8?q?essary=20`pass`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jupyter_cache/base.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index dae26ac..a4fc676 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -44,12 +44,10 @@ class NbArtifactsAbstract(ABC): @abstractmethod def relative_paths(self) -> List[Path]: """Return the list of paths (relative to the notebook folder).""" - pass @abstractmethod def __iter__(self) -> Iterable[Tuple[Path, io.BufferedReader]]: """Yield the relative path and open files (in bytes mode)""" - pass def __repr__(self): return "{0}(paths={1})".format( @@ -110,9 +108,8 @@ class JupyterCacheAbstract(ABC): """An abstract cache for storing pre/post executed notebooks.""" @abstractmethod - def clear_cache(self): + def clear_cache(self) -> None: """Clear the cache completely.""" - pass @abstractmethod def cache_notebook_bundle( @@ -128,7 +125,6 @@ def cache_notebook_bundle( :param overwrite: Allow overwrite of cache with matching hash :return: The primary key of the cache """ - pass @abstractmethod def cache_notebook_file( @@ -154,21 +150,18 @@ def cache_notebook_file( :param overwrite: Allow overwrite of cache with matching hash :return: The primary key of the cache """ - pass @abstractmethod def list_cache_records(self) -> List[NbCacheRecord]: """Return a list of cached notebook records.""" - pass + @abstractmethod def get_cache_record(self, pk: int) -> NbCacheRecord: """Return the record of a cache, by its primary key""" - pass @abstractmethod def get_cache_bundle(self, pk: int) -> NbBundleOut: """Return an executed notebook bundle, by its primary key""" - pass @abstractmethod def cache_artefacts_temppath(self, pk: int) -> Path: @@ -180,7 +173,6 @@ def cache_artefacts_temppath(self, pk: int) -> Path: with cache.cache_artefacts_temppath(1) as path: shutil.copytree(path, destination) """ - pass @abstractmethod def match_cache_notebook(self, nb: nbf.NotebookNode) -> NbCacheRecord: @@ -188,7 +180,6 @@ def match_cache_notebook(self, nb: nbf.NotebookNode) -> NbCacheRecord: :raises KeyError: if no match is found """ - pass def match_cache_file(self, path: str) -> NbCacheRecord: """Match to an executed notebook, returning its primary key. @@ -213,7 +204,6 @@ def merge_match_into_notebook( :raises KeyError: if no match is found :return: pk, input notebook with cached code cells and metadata merged. """ - pass def merge_match_into_file( self, @@ -240,7 +230,6 @@ def diff_nbnode_with_cache( Note: this will not diff markdown content, since it is not stored in the cache. """ - pass def diff_nbfile_with_cache( self, pk: int, path: str, as_str=False, **kwargs @@ -260,22 +249,18 @@ def stage_notebook_file(self, uri: str, assets: List[str] = ()) -> NbStageRecord :param assets: The path of files required by the notebook to run. :raises ValueError: assets not within the same folder as the notebook URI. """ - pass @abstractmethod def discard_staged_notebook(self, uri_or_pk: Union[int, str]): """Discard a staged notebook.""" - pass @abstractmethod def list_staged_records(self) -> List[NbStageRecord]: """list staged notebook URI's in the cache.""" - pass @abstractmethod def get_staged_record(self, uri_or_pk: Union[int, str]) -> NbStageRecord: """Return the record of a staged notebook, by its primary key or URI.""" - pass @abstractmethod def get_staged_notebook( @@ -286,13 +271,12 @@ def get_staged_notebook( :param converter: An optional converter for staged notebooks, which takes the URI and returns a notebook node (default nbformat.read) """ - pass @abstractmethod def get_cache_record_of_staged( self, uri_or_pk: Union[int, str], converter: Optional[Callable] = None ) -> Optional[NbCacheRecord]: - pass + """Get cache record from staged notebook.""" @abstractmethod def list_staged_unexecuted( @@ -303,7 +287,6 @@ def list_staged_unexecuted( :param converter: An optional converter for staged notebooks, which takes the URI and returns a notebook node (default nbformat.read) """ - pass # removed until defined use case # @abstractmethod @@ -314,4 +297,3 @@ def list_staged_unexecuted( # `[codecell_0, textcell_1, codecell_2]` # would map {0: codecell_0, 1: codecell_2} # """ - # pass From 6d12357d55e4a15b06ece39353683afcada6b8fd Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 2 Aug 2021 02:40:37 +0200 Subject: [PATCH 03/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20REFACTOR:=20Make=20n?= =?UTF-8?q?otebook=20reader=20pluggable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rather than passing an optional `converter` to methods, we now store staged files with a specific reader key. The key relates to an entry-point (in group `jcache.readers`) of dynamically loaded reader. Also, the `jupyter_executors` entry group has been changed to `jcache.executors`, and `importlib-metadata` is used to load entry points. --- README.md | 373 +----------------------- docs/using/cli.md | 95 +++--- jupyter_cache/base.py | 29 +- jupyter_cache/cache/db.py | 22 +- jupyter_cache/cache/main.py | 46 +-- jupyter_cache/cli/commands/cmd_exec.py | 36 ++- jupyter_cache/cli/commands/cmd_stage.py | 10 +- jupyter_cache/cli/options.py | 20 +- jupyter_cache/entry_points.py | 36 +++ jupyter_cache/executors/base.py | 32 +- jupyter_cache/executors/basic.py | 7 +- jupyter_cache/readers.py | 62 ++++ setup.cfg | 7 +- tests/make_cli_readme.py | 42 ++- tests/notebooks/basic.md | 21 ++ tests/test_cache.py | 28 +- tox.ini | 9 + 17 files changed, 360 insertions(+), 515 deletions(-) create mode 100644 jupyter_cache/entry_points.py create mode 100644 jupyter_cache/readers.py create mode 100644 tests/notebooks/basic.md diff --git a/README.md b/README.md index aee0aa3..57e99bd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[Install](#install) | [Example](#example-cli-usage) | [Contributing](#contributing) - # jupyter-cache [![Github-CI][github-ci]][github-link] @@ -18,7 +16,7 @@ Some desired requirements (not yet all implemented): - Allow parallel access to notebooks (for execution) - Store execution statistics/reports - Store external assets: Notebooks being executed often require external assets: importing scripts/data/etc. These are prepared by the users. -- Store execution artifacts: created during exeution +- Store execution artifacts: created during execution - A transparent and robust cache invalidation: imagine the user updating an external dependency or a Python module, or checking out a different git branch. ## Install @@ -36,374 +34,7 @@ git checkout develop pip install -e .[cli,code_style,testing] ``` -## Example API usage - -to come ... - -## Example CLI usage - - - -From the checked-out repository folder: - -```console -$ jcache --help -Usage: jcache [OPTIONS] COMMAND [ARGS]... - - The command line interface of jupyter-cache. - -Options: - -v, --version Show the version and exit. - -p, --cache-path Print the current cache path and exit. - -a, --autocomplete Print the autocompletion command and exit. - -h, --help Show this message and exit. - -Commands: - cache Commands for adding to and inspecting the cache. - clear Clear the cache completely. - config Commands for configuring the cache. - execute Execute staged notebooks that are outdated. - stage Commands for staging notebooks to be executed. -``` - -**Important**: Execute this in the terminal for auto-completion: - -```console -eval "$(_JCACHE_COMPLETE=source jcache)" -``` - -### Caching Executed Notebooks - -```console -$ jcache cache --help -Usage: cache [OPTIONS] COMMAND [ARGS]... - - Commands for adding to and inspecting the cache. - -Options: - --help Show this message and exit. - -Commands: - add Cache notebook(s) that have already been executed. - add-with-artefacts Cache a notebook, with possible artefact files. - cat-artifact Print the contents of a cached artefact. - diff-nb Print a diff of a notebook to one stored in the cache. - list List cached notebook records in the cache. - remove Remove notebooks stored in the cache. - show Show details of a cached notebook in the cache. -``` - -The first time the cache is required, it will be lazily created: - -```console -$ jcache cache list -Cache path: ../.jupyter_cache -The cache does not yet exist, do you want to create it? [y/N]: y -No Cached Notebooks - -``` - -You can add notebooks straight into the cache. -When caching, a check will be made that the notebooks look to have been executed -correctly, i.e. the cell execution counts go sequentially up from 1. - -```console -$ jcache cache add tests/notebooks/basic.ipynb -Caching: ../tests/notebooks/basic.ipynb -Validity Error: Expected cell 1 to have execution_count 1 not 2 -The notebook may not have been executed, continue caching? [y/N]: y -Success! -``` - -Or to skip validation: - -```console -$ jcache cache add --no-validate tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb -Caching: ../tests/notebooks/basic.ipynb -Caching: ../tests/notebooks/basic_failing.ipynb -Caching: ../tests/notebooks/basic_unrun.ipynb -Caching: ../tests/notebooks/complex_outputs.ipynb -Caching: ../tests/notebooks/external_output.ipynb -Success! -``` - -Once you've cached some notebooks, you can look at the 'cache records' -for what has been cached. - -Each notebook is hashed (code cells and kernel spec only), -which is used to compare against 'staged' notebooks. -Multiple hashes for the same URI can be added -(the URI is just there for inspetion) and the size of the cache is limited -(current default 1000) so that, at this size, -the last accessed records begin to be deleted. -You can remove cached records by their ID. - -```console -$ jcache cache list - ID Origin URI Created Accessed ----- ------------------------------------- ---------------- ---------------- - 5 tests/notebooks/external_output.ipynb 2020-03-12 17:31 2020-03-12 17:31 - 4 tests/notebooks/complex_outputs.ipynb 2020-03-12 17:31 2020-03-12 17:31 - 3 tests/notebooks/basic_unrun.ipynb 2020-03-12 17:31 2020-03-12 17:31 - 2 tests/notebooks/basic_failing.ipynb 2020-03-12 17:31 2020-03-12 17:31 -``` - -Tip: Use the `--latest-only` option, to only show the latest versions of cached notebooks. - -You can also cache notebooks with artefacts -(external outputs of the notebook execution). - -```console -$ jcache cache add-with-artefacts -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt -Caching: ../tests/notebooks/basic.ipynb -Validity Error: Expected cell 1 to have execution_count 1 not 2 -The notebook may not have been executed, continue caching? [y/N]: y -Success! -``` - -Show a full description of a cached notebook by referring to its ID - -```console -$ jcache cache show 6 -ID: 6 -Origin URI: ../tests/notebooks/basic.ipynb -Created: 2020-03-12 17:31 -Accessed: 2020-03-12 17:31 -Hashkey: 818f3412b998fcf4fe9ca3cca11a3fc3 -Artifacts: -- artifact_folder/artifact.txt -``` - -Note artefact paths must be 'upstream' of the notebook folder: - -```console -$ jcache cache add-with-artefacts -nb tests/notebooks/basic.ipynb tests/test_db.py -Caching: ../tests/notebooks/basic.ipynb -Artifact Error: Path '../tests/test_db.py' is not in folder '../tests/notebooks'' -``` - -To view the contents of an execution artefact: - -```console -$ jcache cache cat-artifact 6 artifact_folder/artifact.txt -An artifact - -``` - -You can directly remove a cached notebook by its ID: - -```console -$ jcache cache remove 4 -Removing Cache ID = 4 -Success! -``` - -You can also diff any of the cached notebooks with any (external) notebook: - -```console -$ jcache cache diff-nb 2 tests/notebooks/basic.ipynb -nbdiff ---- cached pk=2 -+++ other: ../tests/notebooks/basic.ipynb -## inserted before nb/cells/0: -+ code cell: -+ execution_count: 2 -+ source: -+ a=1 -+ print(a) -+ outputs: -+ output 0: -+ output_type: stream -+ name: stdout -+ text: -+ 1 - -## deleted nb/cells/0: -- code cell: -- source: -- raise Exception('oopsie!') - - -Success! -``` - -### Staging Notebooks for execution - -```console -$ jcache stage --help -Usage: stage [OPTIONS] COMMAND [ARGS]... - - Commands for staging notebooks to be executed. - -Options: - --help Show this message and exit. - -Commands: - add Stage notebook(s) for execution. - add-with-assets Stage a notebook, with possible asset files. - list List notebooks staged for possible execution. - remove-ids Un-stage notebook(s), by ID. - remove-uris Un-stage notebook(s), by URI. - show Show details of a staged notebook. -``` - -Staged notebooks are recorded as pointers to their URI, -i.e. no physical copying takes place until execution time. - -If you stage some notebooks for execution, then -you can list them to see which have existing records in the cache (by hash), -and which will require execution: - -```console -$ jcache stage add tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb -Staging: ../tests/notebooks/basic.ipynb -Staging: ../tests/notebooks/basic_failing.ipynb -Staging: ../tests/notebooks/basic_unrun.ipynb -Staging: ../tests/notebooks/complex_outputs.ipynb -Staging: ../tests/notebooks/external_output.ipynb -Success! -``` - -```console -$ jcache stage list - ID URI Created Assets Cache ID ----- ------------------------------------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb 2020-03-12 17:31 0 5 - 4 tests/notebooks/complex_outputs.ipynb 2020-03-12 17:31 0 - 3 tests/notebooks/basic_unrun.ipynb 2020-03-12 17:31 0 6 - 2 tests/notebooks/basic_failing.ipynb 2020-03-12 17:31 0 2 - 1 tests/notebooks/basic.ipynb 2020-03-12 17:31 0 6 -``` - -You can remove a staged notebook by its URI or ID: - -```console -$ jcache stage remove-ids 4 -Unstaging ID: 4 -Success! -``` - -You can then run a basic execution of the required notebooks: - -```console -$ jcache cache remove 6 2 -Removing Cache ID = 6 -Removing Cache ID = 2 -Success! -``` - -```console -$ jcache execute -Executing: ../tests/notebooks/basic.ipynb -Execution Succeeded: ../tests/notebooks/basic.ipynb -Executing: ../tests/notebooks/basic_failing.ipynb -error: Execution Failed: ../tests/notebooks/basic_failing.ipynb -Executing: ../tests/notebooks/basic_unrun.ipynb -Execution Succeeded: ../tests/notebooks/basic_unrun.ipynb -Finished! Successfully executed notebooks have been cached. -succeeded: -- ../tests/notebooks/basic.ipynb -- ../tests/notebooks/basic_unrun.ipynb -excepted: -- ../tests/notebooks/basic_failing.ipynb -errored: [] - -``` - -Successfully executed notebooks will be cached to the cache, -along with any 'artefacts' created by the execution, -that are inside the notebook folder, and data supplied by the executor. - -```console -$ jcache stage list - ID URI Created Assets Cache ID ----- ------------------------------------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb 2020-03-12 17:31 0 5 - 3 tests/notebooks/basic_unrun.ipynb 2020-03-12 17:31 0 6 - 2 tests/notebooks/basic_failing.ipynb 2020-03-12 17:31 0 - 1 tests/notebooks/basic.ipynb 2020-03-12 17:31 0 6 -``` - -Execution data (such as execution time) will be stored in the cache record: - -```console -$ jcache cache show 6 -ID: 6 -Origin URI: ../tests/notebooks/basic_unrun.ipynb -Created: 2020-03-12 17:31 -Accessed: 2020-03-12 17:31 -Hashkey: 818f3412b998fcf4fe9ca3cca11a3fc3 -Data: - execution_seconds: 1.0559415130000005 - -``` - -Failed notebooks will not be cached, but the exception traceback will be added to the stage record: - -```console -$ jcache stage show 2 -ID: 2 -URI: ../tests/notebooks/basic_failing.ipynb -Created: 2020-03-12 17:31 -Failed Last Execution! -Traceback (most recent call last): - File "../jupyter_cache/executors/basic.py", line 152, in execute - executenb(nb_bundle.nb, cwd=tmpdirname) - File "/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/execute.py", line 737, in executenb - return ep.preprocess(nb, resources, km=km)[0] - File "/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/execute.py", line 405, in preprocess - nb, resources = super(ExecutePreprocessor, self).preprocess(nb, resources) - File "/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/base.py", line 69, in preprocess - nb.cells[index], resources = self.preprocess_cell(cell, resources, index) - File "/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/execute.py", line 448, in preprocess_cell - raise CellExecutionError.from_cell_and_msg(cell, out) -nbconvert.preprocessors.execute.CellExecutionError: An error occurred while executing the following cell: ------------------- -raise Exception('oopsie!') ------------------- - ---------------------------------------------------------------------------- -Exception Traceback (most recent call last) - in -----> 1 raise Exception('oopsie!') - -Exception: oopsie! -Exception: oopsie! - - -``` - -Once executed you may leave staged notebooks, for later re-execution, or remove them: - -```console -$ jcache stage remove-ids --all -Are you sure you want to remove all? [y/N]: y -Unstaging ID: 1 -Unstaging ID: 2 -Unstaging ID: 3 -Unstaging ID: 5 -Success! -``` - -You can also stage notebooks with assets; -external files that are required by the notebook during execution. -As with artefacts, these files must be in the same folder as the notebook, -or a sub-folder. - -```console -$ jcache stage add-with-assets -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt -Success! -``` - -```console -$ jcache stage show 1 -ID: 1 -URI: ../tests/notebooks/basic.ipynb -Created: 2020-03-12 17:31 -Cache ID: 6 -Assets: -- ../tests/notebooks/artifact_folder/artifact.txt -``` +See the documentation for usage. ## Contributing diff --git a/docs/using/cli.md b/docs/using/cli.md index 4e3c412..669d2ba 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -2,7 +2,7 @@ # Command-Line - + From the checked-out repository folder: @@ -22,7 +22,7 @@ Commands: cache Commands for adding to and inspecting the cache. clear Clear the cache completely. config Commands for configuring the cache. - execute Execute staged notebooks that are outdated. + execute Execute notebooks that are not in the cache or outdated. stage Commands for staging notebooks to be executed. ``` @@ -95,7 +95,7 @@ for what has been cached. Each notebook is hashed (code cells and kernel spec only), which is used to compare against 'staged' notebooks. Multiple hashes for the same URI can be added -(the URI is just there for inspetion) and the size of the cache is limited +(the URI is just there for inspection) and the size of the cache is limited (current default 1000) so that, at this size, the last accessed records begin to be deleted. You can remove cached records by their ID. @@ -104,10 +104,10 @@ You can remove cached records by their ID. $ jcache cache list ID Origin URI Created Accessed ---- ------------------------------------- ---------------- ---------------- - 5 tests/notebooks/external_output.ipynb 2020-03-12 17:31 2020-03-12 17:31 - 4 tests/notebooks/complex_outputs.ipynb 2020-03-12 17:31 2020-03-12 17:31 - 3 tests/notebooks/basic_unrun.ipynb 2020-03-12 17:31 2020-03-12 17:31 - 2 tests/notebooks/basic_failing.ipynb 2020-03-12 17:31 2020-03-12 17:31 + 5 tests/notebooks/external_output.ipynb 2021-08-01 15:43 2021-08-01 15:43 + 4 tests/notebooks/complex_outputs.ipynb 2021-08-01 15:43 2021-08-01 15:43 + 3 tests/notebooks/basic_unrun.ipynb 2021-08-01 15:43 2021-08-01 15:43 + 2 tests/notebooks/basic_failing.ipynb 2021-08-01 15:43 2021-08-01 15:43 ``` ````{tip} @@ -135,9 +135,9 @@ Show a full description of a cached notebook by referring to its ID $ jcache cache show 6 ID: 6 Origin URI: ../tests/notebooks/basic.ipynb -Created: 2020-03-12 17:31 -Accessed: 2020-03-12 17:31 -Hashkey: 818f3412b998fcf4fe9ca3cca11a3fc3 +Created: 2021-08-01 15:43 +Accessed: 2021-08-01 15:43 +Hashkey: 94c17138f782c75df59e989fffa64e3a Artifacts: - artifact_folder/artifact.txt ``` @@ -234,13 +234,13 @@ Success! ```console $ jcache stage list - ID URI Created Assets Cache ID ----- ------------------------------------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb 2020-03-12 17:31 0 5 - 4 tests/notebooks/complex_outputs.ipynb 2020-03-12 17:31 0 - 3 tests/notebooks/basic_unrun.ipynb 2020-03-12 17:31 0 6 - 2 tests/notebooks/basic_failing.ipynb 2020-03-12 17:31 0 2 - 1 tests/notebooks/basic.ipynb 2020-03-12 17:31 0 6 + ID URI Reader Created Assets Cache ID +---- ------------------------------------- -------- ---------------- -------- ---------- + 5 tests/notebooks/external_output.ipynb nbformat 2021-08-01 15:43 0 5 + 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-01 15:43 0 + 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-01 15:43 0 6 + 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-01 15:43 0 2 + 1 tests/notebooks/basic.ipynb nbformat 2021-08-01 15:43 0 6 ``` You can remove a staged notebook by its URI or ID: @@ -275,6 +275,7 @@ succeeded: excepted: - ../tests/notebooks/basic_failing.ipynb errored: [] +up-to-date: [] ``` @@ -284,12 +285,12 @@ that are inside the notebook folder, and data supplied by the executor. ```console $ jcache stage list - ID URI Created Assets Cache ID ----- ------------------------------------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb 2020-03-12 17:31 0 5 - 3 tests/notebooks/basic_unrun.ipynb 2020-03-12 17:31 0 6 - 2 tests/notebooks/basic_failing.ipynb 2020-03-12 17:31 0 - 1 tests/notebooks/basic.ipynb 2020-03-12 17:31 0 6 + ID URI Reader Created Assets Cache ID +---- ------------------------------------- -------- ---------------- -------- ---------- + 5 tests/notebooks/external_output.ipynb nbformat 2021-08-01 15:43 0 5 + 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-01 15:43 0 6 + 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-01 15:43 0 + 1 tests/notebooks/basic.ipynb nbformat 2021-08-01 15:43 0 6 ``` Execution data (such as execution time) will be stored in the cache record: @@ -298,11 +299,11 @@ Execution data (such as execution time) will be stored in the cache record: $ jcache cache show 6 ID: 6 Origin URI: ../tests/notebooks/basic_unrun.ipynb -Created: 2020-03-12 17:31 -Accessed: 2020-03-12 17:31 -Hashkey: 818f3412b998fcf4fe9ca3cca11a3fc3 +Created: 2021-08-01 15:43 +Accessed: 2021-08-01 15:43 +Hashkey: 94c17138f782c75df59e989fffa64e3a Data: - execution_seconds: 1.0559415130000005 + execution_seconds: 0.9876813249999996 ``` @@ -312,27 +313,34 @@ Failed notebooks will not be cached, but the exception traceback will be added t $ jcache stage show 2 ID: 2 URI: ../tests/notebooks/basic_failing.ipynb -Created: 2020-03-12 17:31 +Reader: nbformat +Created: 2021-08-01 15:43 Failed Last Execution! Traceback (most recent call last): - File "../jupyter_cache/executors/basic.py", line 152, in execute - executenb(nb_bundle.nb, cwd=tmpdirname) - File "/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/execute.py", line 737, in executenb - return ep.preprocess(nb, resources, km=km)[0] - File "/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/execute.py", line 405, in preprocess - nb, resources = super(ExecutePreprocessor, self).preprocess(nb, resources) - File "/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/base.py", line 69, in preprocess - nb.cells[index], resources = self.preprocess_cell(cell, resources, index) - File "/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/execute.py", line 448, in preprocess_cell - raise CellExecutionError.from_cell_and_msg(cell, out) -nbconvert.preprocessors.execute.CellExecutionError: An error occurred while executing the following cell: + File "../jupyter_cache/executors/utils.py", line 55, in single_nb_execution + record_timing=False, + File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/client.py", line 1112, in execute + return NotebookClient(nb=nb, resources=resources, km=km, **kwargs).execute() + File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/util.py", line 74, in wrapped + return just_run(coro(*args, **kwargs)) + File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/util.py", line 53, in just_run + return loop.run_until_complete(coro) + File "../.tox/create_cli_doc/lib/python3.7/asyncio/base_events.py", line 587, in run_until_complete + return future.result() + File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/client.py", line 554, in async_execute + cell, index, execution_count=self.code_cells_executed + 1 + File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/client.py", line 857, in async_execute_cell + self._check_raise_for_error(cell, exec_reply) + File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/client.py", line 760, in _check_raise_for_error + raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content) +nbclient.exceptions.CellExecutionError: An error occurred while executing the following cell: ------------------ raise Exception('oopsie!') ------------------ --------------------------------------------------------------------------- Exception Traceback (most recent call last) - in +/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_39409/340246212.py in ----> 1 raise Exception('oopsie!') Exception: oopsie! @@ -342,8 +350,8 @@ Exception: oopsie! ``` ```{tip} -Code cells can be tagged with `raises-exception` to let the executor known that -a cell *may* raise an exception (see [this issue on its behaviour](https://github.com/jupyter/nbconvert/issues/730)). +Code cells can be tagged with `raises-exception` to let the executor known that a cell *may* raise an exception +(see [this issue on its behaviour](https://github.com/jupyter/nbconvert/issues/730)). ``` Once executed you may leave staged notebooks, for later re-execution, or remove them: @@ -372,7 +380,8 @@ Success! $ jcache stage show 1 ID: 1 URI: ../tests/notebooks/basic.ipynb -Created: 2020-03-12 17:31 +Reader: nbformat +Created: 2021-08-01 15:43 Cache ID: 6 Assets: - ../tests/notebooks/artifact_folder/artifact.txt diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index a4fc676..a6a69c9 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -6,7 +6,7 @@ import io from abc import ABC, abstractmethod from pathlib import Path -from typing import Callable, Iterable, List, Optional, Tuple, Union +from typing import Iterable, List, Optional, Tuple, Union import attr import nbformat as nbf @@ -242,10 +242,13 @@ def diff_nbfile_with_cache( return self.diff_nbnode_with_cache(pk, nb, uri=path, as_str=as_str, **kwargs) @abstractmethod - def stage_notebook_file(self, uri: str, assets: List[str] = ()) -> NbStageRecord: + def stage_notebook_file( + self, uri: str, *, reader: Optional[str] = None, assets: List[str] = () + ) -> NbStageRecord: """Stage a single notebook for execution. :param uri: The path to the file + :param reader: A key for the reader function, to read the uri and return a NotebookNode :param assets: The path of files required by the notebook to run. :raises ValueError: assets not within the same folder as the notebook URI. """ @@ -263,30 +266,18 @@ def get_staged_record(self, uri_or_pk: Union[int, str]) -> NbStageRecord: """Return the record of a staged notebook, by its primary key or URI.""" @abstractmethod - def get_staged_notebook( - self, uri_or_pk: Union[int, str], converter: Optional[Callable] = None - ) -> NbBundleIn: - """Return a single staged notebook, by its primary key or URI. - - :param converter: An optional converter for staged notebooks, - which takes the URI and returns a notebook node (default nbformat.read) - """ + def get_staged_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: + """Return a single staged notebook, by its primary key or URI.""" @abstractmethod def get_cache_record_of_staged( - self, uri_or_pk: Union[int, str], converter: Optional[Callable] = None + self, uri_or_pk: Union[int, str] ) -> Optional[NbCacheRecord]: """Get cache record from staged notebook.""" @abstractmethod - def list_staged_unexecuted( - self, converter: Optional[Callable] = None - ) -> List[NbStageRecord]: - """List staged notebooks, whose hash is not present in the cache. - - :param converter: An optional converter for staged notebooks, - which takes the URI and returns a notebook node (default nbformat.read) - """ + def list_staged_unexecuted(self) -> List[NbStageRecord]: + """List staged notebooks, whose hash is not present in the cache.""" # removed until defined use case # @abstractmethod diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 4ec08e0..a7b179b 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -6,7 +6,7 @@ from sqlalchemy import JSON, Column, DateTime, Integer, String, Text from sqlalchemy.engine import Engine, create_engine -from sqlalchemy.exc import IntegrityError +from sqlalchemy.exc import IntegrityError, OperationalError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Session, sessionmaker, validates from sqlalchemy.sql.expression import desc @@ -15,6 +15,10 @@ OrmBase = declarative_base() +# TODO store this in the database so we can check for updates +DB_VERSION = 2 +# v2 added reader field to nbstage + def create_db(path, name="global.db") -> Engine: engine = create_engine("sqlite:///{}".format(os.path.join(path, name))) @@ -28,6 +32,11 @@ def session_context(engine: Engine): session = sessionmaker(bind=engine)() try: yield session + except OperationalError as exc: + session.rollback() + raise RuntimeError( + "Unexpected error accessing jupyter cache, it may need to be cleared." + ) from exc except Exception: session.rollback() raise @@ -224,6 +233,7 @@ class NbStageRecord(OrmBase): pk = Column(Integer(), primary_key=True) uri = Column(String(255), nullable=False, unique=True) + reader = Column(String(255), nullable=False) assets = Column(JSON(), nullable=False, default=list) traceback = Column(Text(), nullable=True, default="") created = Column(DateTime, nullable=False, default=datetime.utcnow) @@ -238,6 +248,7 @@ def format_dict(self, cache_record=None, path_length=None, assets=True): data = { "ID": self.pk, "URI": str(shorten_path(self.uri, path_length)), + "Reader": self.reader, "Created": self.created.isoformat(" ", "minutes"), } if assets: @@ -270,11 +281,16 @@ def validate_assets(paths, uri=None): @staticmethod def create_record( - uri: str, db: Engine, raise_on_exists=True, assets=() + uri: str, + db: Engine, + raise_on_exists=True, + *, + reader: str = "nbformat", + assets=(), ) -> "NbStageRecord": assets = NbStageRecord.validate_assets(assets, uri) with session_context(db) as session: # type: Session - record = NbStageRecord(uri=uri, assets=assets) + record = NbStageRecord(uri=uri, reader=reader, assets=assets) session.add(record) try: session.commit() diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 8681d7d..63c46cf 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -4,7 +4,7 @@ import shutil from contextlib import contextmanager from pathlib import Path -from typing import Callable, Iterable, List, Optional, Tuple, Union +from typing import Iterable, List, Optional, Tuple, Union import nbformat as nbf @@ -18,6 +18,7 @@ NbValidityError, RetrievalError, ) +from jupyter_cache.readers import get_reader from jupyter_cache.utils import to_relative_paths from .db import NbCacheRecord, NbStageRecord, Setting, create_db @@ -398,15 +399,24 @@ def diff_nbnode_with_cache( ) return stream.getvalue() - def stage_notebook_file(self, path: str, assets=()) -> NbStageRecord: + def stage_notebook_file( + self, path: str, *, reader: str = "nbformat", assets=() + ) -> NbStageRecord: """Stage a single notebook for execution. :param uri: The path to the file + :param reader: A key for the reader function. :param assets: The path of files required by the notebook to run. These must be within the same folder as the notebook. """ + _ = get_reader(reader) + # TODO should we test that the file can be read by the reader? return NbStageRecord.create_record( - str(Path(path).absolute()), self.db, raise_on_exists=False, assets=assets + str(Path(path).absolute()), + self.db, + raise_on_exists=False, + reader=reader, + assets=assets, ) # TODO physically copy to cache? # TODO assets @@ -430,43 +440,39 @@ def discard_staged_notebook(self, uri_or_pk: Union[int, str]): # TODO add discard all/multiple staged records method - def get_staged_notebook( - self, uri_or_pk: Union[int, str], converter: Optional[Callable] = None - ) -> NbBundleIn: + def get_staged_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: """Return a single staged notebook.""" if isinstance(uri_or_pk, int): - uri_or_pk = NbStageRecord.record_from_pk(uri_or_pk, self.db).uri - if not Path(uri_or_pk).exists(): + record = NbStageRecord.record_from_pk(uri_or_pk, self.db) + else: + record = NbStageRecord.record_from_uri(uri_or_pk, self.db) + if not Path(record.uri).exists(): raise IOError( - "The URI of the staged record no longer exists: {}".format(uri_or_pk) + "The URI of the staged record no longer exists: {}".format(record.uri) ) - if converter is None: - notebook = nbf.read(uri_or_pk, nbf.NO_CONVERT) - else: - notebook = converter(uri_or_pk) - return NbBundleIn(notebook, uri_or_pk) + converter = get_reader(record.reader) + notebook = converter(record.uri) + return NbBundleIn(notebook, record.uri) def get_cache_record_of_staged( - self, uri_or_pk: Union[int, str], converter: Optional[Callable] = None + self, uri_or_pk: Union[int, str] ) -> Optional[NbCacheRecord]: if isinstance(uri_or_pk, int): record = NbStageRecord.record_from_pk(uri_or_pk, self.db) else: record = NbStageRecord.record_from_uri(uri_or_pk, self.db) - nb = self.get_staged_notebook(record.uri, converter=converter).nb + nb = self.get_staged_notebook(record.uri).nb _, hashkey = self.create_hashed_notebook(nb) try: return NbCacheRecord.record_from_hashkey(hashkey, self.db) except KeyError: return None - def list_staged_unexecuted( - self, converter: Optional[Callable] = None - ) -> List[NbStageRecord]: + def list_staged_unexecuted(self) -> List[NbStageRecord]: """List staged notebooks, whose hash is not present in the cached notebooks.""" records = [] for record in self.list_staged_records(): - nb = self.get_staged_notebook(record.uri, converter).nb + nb = self.get_staged_notebook(record.uri).nb _, hashkey = self.create_hashed_notebook(nb) try: NbCacheRecord.record_from_hashkey(hashkey, self.db) diff --git a/jupyter_cache/cli/commands/cmd_exec.py b/jupyter_cache/cli/commands/cmd_exec.py index ad03a83..9320672 100644 --- a/jupyter_cache/cli/commands/cmd_exec.py +++ b/jupyter_cache/cli/commands/cmd_exec.py @@ -7,18 +7,19 @@ from jupyter_cache import get_cache from jupyter_cache.cli import arguments, options from jupyter_cache.cli.commands.cmd_main import jcache +from jupyter_cache.readers import list_readers logger = logging.getLogger(__name__) click_log.basic_config(logger) @jcache.command("execute") -@click_log.simple_verbosity_option(logger) -@options.EXEC_ENTRYPOINT +@arguments.PK_OR_PATHS +@options.EXECUTOR_KEY @options.EXEC_TIMEOUT @options.CACHE_PATH -@arguments.PK_OR_PATHS -def execute_nbs(cache_path, entry_point, pk_paths, timeout): +@click_log.simple_verbosity_option(logger) +def execute_nbs(cache_path, executor, pk_paths, timeout): """Execute notebooks that are not in the cache or outdated.""" import yaml @@ -26,20 +27,39 @@ def execute_nbs(cache_path, entry_point, pk_paths, timeout): db = get_cache(cache_path) records = [] + unstaged = [] for pk_path in pk_paths: if pk_path.isdigit(): pk_path = int(pk_path) record = db.get_staged_record(int(pk_path)) + records.append(record) else: try: - record = db.get_staged_record(pk_path) + record = db.get_staged_record(str(Path(pk_path).absolute())) + records.append(record) except KeyError: if not Path(pk_path).exists(): raise FileNotFoundError(f"'{pk_path}' does not exist.") - record = db.stage_notebook_file(pk_path) - records.append(record) + unstaged.append(pk_path) + if unstaged: + # ask to stage, select reader + unstaged_string = "\n - ".join(unstaged) + click.echo(f"Unstaged notebooks specified:\n - {unstaged_string}") + if not click.confirm("Continue (staging these notebooks first)?"): + click.secho("Aborted!", bold=True, fg="red") + raise SystemExit(1) + reader = click.prompt( + "Enter the notebook reader to use", + type=click.Choice(list_readers()), + show_choices=True, + default="nbformat", + show_default=True, + ) + for pk_path in unstaged: + record = db.stage_notebook_file(pk_path, reader=reader) + records.append(record) try: - executor = load_executor(entry_point, db, logger=logger) + executor = load_executor(executor, db, logger=logger) except ImportError as error: logger.error(str(error)) return 1 diff --git a/jupyter_cache/cli/commands/cmd_stage.py b/jupyter_cache/cli/commands/cmd_stage.py index 40a98de..be70f41 100644 --- a/jupyter_cache/cli/commands/cmd_stage.py +++ b/jupyter_cache/cli/commands/cmd_stage.py @@ -16,25 +16,27 @@ def cmnd_stage(): @cmnd_stage.command("add") @arguments.NB_PATHS +@options.READER_KEY @options.CACHE_PATH -def stage_nbs(cache_path, nbpaths): +def stage_nbs(cache_path, nbpaths, reader): """Stage notebook(s) for execution.""" db = get_cache(cache_path) for path in nbpaths: # TODO deal with errors (print all at end? or option to ignore) click.echo("Staging: {}".format(path)) - db.stage_notebook_file(path) + db.stage_notebook_file(path, reader=reader) click.secho("Success!", fg="green") @cmnd_stage.command("add-with-assets") @arguments.ASSET_PATHS @options.NB_PATH +@options.READER_KEY @options.CACHE_PATH -def stage_nb(cache_path, nbpath, asset_paths): +def stage_nb(cache_path, nbpath, reader, asset_paths): """Stage a notebook, with possible asset files.""" db = get_cache(cache_path) - db.stage_notebook_file(nbpath, asset_paths) + db.stage_notebook_file(nbpath, reader=reader, assets=asset_paths) click.secho("Success!", fg="green") diff --git a/jupyter_cache/cli/options.py b/jupyter_cache/cli/options.py index eae0d89..0ae51d0 100644 --- a/jupyter_cache/cli/options.py +++ b/jupyter_cache/cli/options.py @@ -2,6 +2,9 @@ import click +from jupyter_cache.entry_points import ENTRY_POINT_GROUP_EXEC, list_group_names +from jupyter_cache.readers import list_readers + def callback_autocomplete(ctx, param, value): if value and not ctx.resilient_parsing: @@ -72,13 +75,22 @@ def check_cache_exists(ctx, param, value): type=click.Path(dir_okay=False, exists=True, readable=True, resolve_path=True), ) +READER_KEY = click.option( + "-r", + "--reader", + help="The notebook reader to use.", + default="nbformat", + type=click.Choice(list_readers()), + show_default=True, +) + -EXEC_ENTRYPOINT = click.option( +EXECUTOR_KEY = click.option( "-e", - "--entry-point", - # TODO list additional entry points - help="The entry-point from which to load the executor [local-serial|temp-serial].", + "--executor", + help="The executor to use.", default="local-serial", + type=click.Choice(list_group_names(ENTRY_POINT_GROUP_EXEC)), show_default=True, ) diff --git a/jupyter_cache/entry_points.py b/jupyter_cache/entry_points.py new file mode 100644 index 0000000..846768d --- /dev/null +++ b/jupyter_cache/entry_points.py @@ -0,0 +1,36 @@ +"""Module for dealing with entry points.""" +from typing import Optional, Set + +# TODO importlib.metadata was introduced into the standard library in python 3.8 +# so we can change this when we drop support for 3.7 +# also, from importlib_metadata changed its API in v4.0, to use the python 3.10 API +# however, because of https://github.com/python/importlib_metadata/issues/308 +# we do not assume that we have this API, and instead use try/except for the new/old APIs +from importlib_metadata import EntryPoint +from importlib_metadata import entry_points as eps + +ENTRY_POINT_GROUP_READER = "jcache.readers" +ENTRY_POINT_GROUP_EXEC = "jcache.executors" + + +def list_group_names(group: str) -> Set[str]: + """Return the entry points within a group.""" + all_eps = eps() + try: + # importlib_metadata v4 / python 3.10 + return all_eps.select(group=group).names + except (AttributeError, TypeError): + return {ep.name for ep in all_eps.get(group, [])} + + +def get_entry_point(group: str, name: str) -> Optional[EntryPoint]: + """Return the entry point with the given name in the given group.""" + all_eps = eps() + try: + # importlib_metadata v4 / python 3.10 + found = all_eps.select(group=group, name=name) + ep = found[name] if name in found.names else None + except (AttributeError, TypeError): + found = {ep.name: ep for ep in all_eps.get(group, [])} + ep = found[name] if name in found else None + return ep diff --git a/jupyter_cache/executors/base.py b/jupyter_cache/executors/base.py index a81597e..e15ec5c 100644 --- a/jupyter_cache/executors/base.py +++ b/jupyter_cache/executors/base.py @@ -1,15 +1,15 @@ import logging from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set import attr -# TODO use importlib.metadata -import pkg_resources - from jupyter_cache.base import JupyterCacheAbstract - -ENTRY_POINT_GROUP = "jupyter_executors" +from jupyter_cache.entry_points import ( + ENTRY_POINT_GROUP_EXEC, + get_entry_point, + list_group_names, +) base_logger = logging.getLogger(__name__) @@ -66,7 +66,6 @@ def run_and_cache( *, filter_uris: Optional[List[str]] = None, filter_pks: Optional[List[int]] = None, - converter: Optional[Callable] = None, timeout: Optional[int] = 30, allow_errors: bool = False, **kwargs: Any @@ -79,9 +78,6 @@ def run_and_cache( If specified filter the staged notebooks to execute by these URIs filter_pks: list If specified filter the staged notebooks to execute by these PKs - converter: - An optional converter for staged notebooks, - which takes the URI and returns a notebook node timeout: int Maximum time in seconds to wait for a single cell to run for allow_errors: bool @@ -90,22 +86,18 @@ def run_and_cache( """ -def list_executors(): - return list(pkg_resources.iter_entry_points(ENTRY_POINT_GROUP)) +def list_executors() -> Set[str]: + return list_group_names(ENTRY_POINT_GROUP_EXEC) def load_executor( entry_point: str, cache: JupyterCacheAbstract, logger=None ) -> JupyterExecutorAbstract: """Retrieve an initialised JupyterExecutor from an entry point.""" - entry_points = list(pkg_resources.iter_entry_points(ENTRY_POINT_GROUP, entry_point)) - if len(entry_points) == 0: - raise ImportError( - "Entry point not found: {}.{}".format(ENTRY_POINT_GROUP, entry_point) - ) - if len(entry_points) != 1: + ep = get_entry_point(ENTRY_POINT_GROUP_EXEC, entry_point) + if ep is None: raise ImportError( - "Multiple entry points found: {}.{}".format(ENTRY_POINT_GROUP, entry_point) + "Entry point not found: {}:{}".format(ENTRY_POINT_GROUP_EXEC, entry_point) ) - execute_cls = entry_points[0].load() + execute_cls = ep.load() return execute_cls(cache=cache, logger=logger) diff --git a/jupyter_cache/executors/basic.py b/jupyter_cache/executors/basic.py index 2216b91..3d81258 100644 --- a/jupyter_cache/executors/basic.py +++ b/jupyter_cache/executors/basic.py @@ -34,13 +34,12 @@ def run_and_cache( *, filter_uris=None, filter_pks=None, - converter=None, timeout=30, allow_errors=False, ) -> ExecutorRunResult: """This function interfaces with the cache, deferring execution to `execute`.""" # Get the notebook tha require re-execution - stage_records = self.cache.list_staged_unexecuted(converter=converter) + stage_records = self.cache.list_staged_unexecuted() if filter_uris is not None: stage_records = [r for r in stage_records if r.uri in filter_uris] if filter_pks is not None: @@ -59,9 +58,7 @@ def run_and_cache( def _iterator(): for stage_record in stage_records: try: - nb_bundle = self.cache.get_staged_notebook( - stage_record.pk, converter - ) + nb_bundle = self.cache.get_staged_notebook(stage_record.pk) except Exception: self.logger.error( "Failed Retrieving: {}".format(stage_record.uri), exc_info=True diff --git a/jupyter_cache/readers.py b/jupyter_cache/readers.py new file mode 100644 index 0000000..2fb6e28 --- /dev/null +++ b/jupyter_cache/readers.py @@ -0,0 +1,62 @@ +"""Module for handling different functions to read "notebook-like" files.""" +import threading +from typing import Callable, Set + +import nbformat as nbf + +from .entry_points import ENTRY_POINT_GROUP_READER, get_entry_point, list_group_names + +# a thread safe cache for notebook read functions +# we include this in addition to entry points, since myst_nb needs to add them dynamically +_THREAD_CACHE = threading.local() +_THREAD_CACHE.readers = {} +_READERS = _THREAD_CACHE.readers + + +def nbf_reader(uri: str) -> nbf.NotebookNode: + """Standard notebook reader.""" + return nbf.read(uri, nbf.NO_CONVERT) + + +def jupytext_reader(uri: str) -> nbf.NotebookNode: + """Jupytext notebook reader.""" + try: + import jupytext + except ImportError: + raise ImportError("jupytext must be installed to use this reader") + return jupytext.read(uri) + + +def add_reader( + key: str, + reader: Callable[[str], nbf.NotebookNode], + override: bool = False, +) -> None: + """Add a reader function to the cache. + + :param extension: The key to store the reader under. + :param reader: A function that takes a path as input and returns a notebook node. + :param override: If True, override an existing reader. + + """ + if not override and ( + key in _READERS or get_entry_point(ENTRY_POINT_GROUP_READER, key) + ): + raise ValueError(f"Reader '{key}' already exists") + + _READERS[key] = reader + + +def list_readers() -> Set[str]: + """List all available readers.""" + return set(list(_READERS) + list(list_group_names(ENTRY_POINT_GROUP_READER))) + + +def get_reader(key: str) -> Callable[[str], nbf.NotebookNode]: + """Returns a function to read a file URI and return a notebook.""" + if key in _READERS: + return _READERS[key] + reader = get_entry_point(ENTRY_POINT_GROUP_READER, key) + if reader is not None: + return reader.load() + raise ValueError(f"No reader found for '{key}'") diff --git a/setup.cfg b/setup.cfg index 8ba61ff..543e217 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ project_urls = packages = find: install_requires = attrs + importlib-metadata nbclient>=0.2,<0.6 nbdime nbformat @@ -40,9 +41,12 @@ zip_safe = True [options.entry_points] console_scripts = jcache = jupyter_cache.cli.commands.cmd_main:jcache -jupyter_executors = +jcache.executors = local-serial = jupyter_cache.executors.basic:JupyterExecutorLocalSerial temp-serial = jupyter_cache.executors.basic:JupyterExecutorTempSerial +jcache.readers = + nbformat = jupyter_cache.readers:nbf_reader + jupytext = jupyter_cache.readers:jupytext_reader [options.extras_require] cli = @@ -60,6 +64,7 @@ rtd = testing = coverage ipykernel + jupytext matplotlib nbformat>=5.1 numpy diff --git a/tests/make_cli_readme.py b/tests/make_cli_readme.py index 41618e2..1f727f4 100644 --- a/tests/make_cli_readme.py +++ b/tests/make_cli_readme.py @@ -1,6 +1,5 @@ import os from datetime import datetime -from glob import glob from textwrap import dedent from click.testing import CliRunner @@ -19,10 +18,13 @@ def get_string(cli, group=None, args=(), input=None): runner = CliRunner() result = runner.invoke(cli, args, input=input) root_path = os.getcwd() + os.sep + output = result.output.replace(root_path, "../") + if result.exception: + output += "\n" + str(result.exception) return "```console\n$ {}{}\n{}```".format( command_str, (" " + " ".join(args)) if args else "", - result.output.replace(root_path, "../"), + output, ) @@ -30,7 +32,7 @@ def main(): get_string(cmd_main.clear_cache, input="y") - strings = [] + strings = ["(use/cli)=", "# Command-Line"] strings.append( "".format( datetime.now().isoformat(" ", "minutes"), __file__ @@ -52,7 +54,7 @@ def main(): ) # cache - strings.append("### Caching Executed Notebooks") + strings.append("## Caching Executed Notebooks") cache_name = cmd_cache.cmnd_cache.name strings.append(get_string(cmd_cache.cmnd_cache, None, ["--help"])) strings.append("The first time the cache is required, it will be lazily created:") @@ -75,7 +77,14 @@ def main(): get_string( cmd_cache.cache_nbs, cache_name, - ["--no-validate"] + glob("tests/notebooks/*.ipynb"), + [ + "--no-validate", + "tests/notebooks/basic.ipynb", + "tests/notebooks/basic_failing.ipynb", + "tests/notebooks/basic_unrun.ipynb", + "tests/notebooks/complex_outputs.ipynb", + "tests/notebooks/external_output.ipynb", + ], ) ) strings.append( @@ -87,7 +96,7 @@ def main(): Each notebook is hashed (code cells and kernel spec only), which is used to compare against 'staged' notebooks. Multiple hashes for the same URI can be added - (the URI is just there for inspetion) and the size of the cache is limited + (the URI is just there for inspection) and the size of the cache is limited (current default {}) so that, at this size, the last accessed records begin to be deleted. You can remove cached records by their ID.""".format( @@ -155,7 +164,7 @@ def main(): ) # staging - strings.append("### Staging Notebooks for execution") + strings.append("## Staging Notebooks for execution") stage_name = cmd_stage.cmnd_stage.name strings.append(get_string(cmd_stage.cmnd_stage, None, ["--help"])) strings.append( @@ -170,7 +179,17 @@ def main(): ) ) strings.append( - get_string(cmd_stage.stage_nbs, stage_name, glob("tests/notebooks/*.ipynb")) + get_string( + cmd_stage.stage_nbs, + stage_name, + [ + "tests/notebooks/basic.ipynb", + "tests/notebooks/basic_failing.ipynb", + "tests/notebooks/basic_unrun.ipynb", + "tests/notebooks/complex_outputs.ipynb", + "tests/notebooks/external_output.ipynb", + ], + ) ) strings.append(get_string(cmd_stage.list_staged, stage_name)) strings.append("You can remove a staged notebook by its URI or ID:") @@ -200,10 +219,9 @@ def main(): dedent( """\ ```{tip} - Code cells can be tagged with `raises-exception` to let the executor known that - a cell *may* raise an exception(see - [this issue on its behaviour](https://github.com/jupyter/nbconvert/issues/730)). - ```""" + Code cells can be tagged with `raises-exception` to let the executor known that a cell *may* raise an exception + (see [this issue on its behaviour](https://github.com/jupyter/nbconvert/issues/730)). + ```""" # noqa: E501 ) ) strings.append( diff --git a/tests/notebooks/basic.md b/tests/notebooks/basic.md new file mode 100644 index 0000000..1f4eba3 --- /dev/null +++ b/tests/notebooks/basic.md @@ -0,0 +1,21 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.11.3 +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +# a title + +some text + +```{code-cell} ipython3 +a=1 +print(a) +``` diff --git a/tests/test_cache.py b/tests/test_cache.py index e6163e7..edaa2f7 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -182,9 +182,6 @@ def test_artifacts(tmp_path): assert path.joinpath("artifact_folder").exists() -# jupyter_client/session.py:371: DeprecationWarning: -# Session._key_changed is deprecated in traitlets: use @observe and @unobserve instead -@pytest.mark.filterwarnings("ignore") @pytest.mark.parametrize("executor_key", ["local-serial", "temp-serial"]) def test_execution(tmp_path, executor_key): from jupyter_cache.executors import load_executor @@ -229,7 +226,29 @@ def test_execution(tmp_path, executor_key): assert "Exception: oopsie!" in stage_record.traceback -@pytest.mark.filterwarnings("ignore") +def test_execution_jupytext(tmp_path): + """Test execution with the jupytext reader.""" + from jupyter_cache.executors import load_executor + + db = JupyterCacheBase(str(tmp_path / "cache")) + temp_nb_path = tmp_path / "notebooks" + shutil.copytree(NB_PATH, temp_nb_path) + db.stage_notebook_file( + path=os.path.join(temp_nb_path, "basic.md"), reader="jupytext" + ) + executor = load_executor("local-serial", db) + result = executor.run_and_cache() + print(result) + assert result.as_json() == { + "succeeded": [ + os.path.join(temp_nb_path, "basic.md"), + ], + "excepted": [], + "errored": [], + } + assert len(db.list_cache_records()) == 1 + + def test_execution_timeout_config(tmp_path): """tests the timeout value passed to the executor""" from jupyter_cache.executors import load_executor @@ -255,7 +274,6 @@ def test_execution_timeout_config(tmp_path): } -@pytest.mark.filterwarnings("ignore") def test_execution_timeout_metadata(tmp_path): """tests the timeout metadata key in notebooks""" from jupyter_cache.executors import load_executor diff --git a/tox.ini b/tox.ini index a6faccb..73dd7a5 100644 --- a/tox.ini +++ b/tox.ini @@ -27,8 +27,17 @@ commands = pytest {posargs} extras = cli deps = ipykernel + jupytext commands = jcache {posargs} +[testenv:create_cli_doc] +description = Create output to add to docs/using/cli.md +extras = cli +deps = + ipykernel + jupytext +commands = python tests/make_cli_readme.py + [testenv:docs-{clean,update}] extras = rtd whitelist_externals = From 3be9692afa593ccc344fa8da1c8ac7bd0c1fc1b6 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 2 Aug 2021 04:56:01 +0200 Subject: [PATCH 04/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20REFACTOR:=20`stage`?= =?UTF-8?q?=20->=20`project`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was felt that this is conceptually easier to understand, i.e. it is a list of records for each notebook (& associated data) in the project, rather than just a staging area for pre-executed notebooks. --- .gitignore | 1 + docs/conf.py | 9 + docs/index.md | 15 +- docs/using/api.ipynb | 698 +++---- docs/using/cli.md | 122 +- docs/using/images/execution_flow.pptx | Bin 51293 -> 48314 bytes docs/using/images/execution_process.svg | 1692 ++++++++++------- jupyter_cache/base.py | 47 +- jupyter_cache/cache/db.py | 46 +- jupyter_cache/cache/main.py | 61 +- jupyter_cache/cli/commands/__init__.py | 2 +- jupyter_cache/cli/commands/cmd_cache.py | 18 + jupyter_cache/cli/commands/cmd_exec.py | 23 +- jupyter_cache/cli/commands/cmd_main.py | 8 +- .../commands/{cmd_stage.py => cmd_project.py} | 85 +- jupyter_cache/cli/options.py | 4 + jupyter_cache/executors/base.py | 14 +- jupyter_cache/executors/basic.py | 41 +- jupyter_cache/utils.py | 6 +- tests/make_cli_readme.py | 46 +- tests/test_cache.py | 42 +- tests/test_cli.py | 30 +- 22 files changed, 1622 insertions(+), 1388 deletions(-) rename jupyter_cache/cli/commands/{cmd_stage.py => cmd_project.py} (50%) diff --git a/.gitignore b/.gitignore index 27d37f3..4e25241 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,4 @@ _archive/ *_old* .DS_Store .vscode/ +~$* diff --git a/docs/conf.py b/docs/conf.py index 6d740f3..cade15e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,6 +26,15 @@ # "sphinx.ext.viewcode", ] jupyter_execute_notebooks = "off" +html_theme_options = { + "repository_url": "https://github.com/executablebooks/jupyter-cache", + "use_repository_button": True, + "use_edit_page_button": True, + "use_issues_button": True, + "repository_branch": "master", + "path_to_docs": "docs", + "home_page_in_toc": True, +} # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/index.md b/docs/index.md index f026632..95d9435 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,12 +2,9 @@ A defined interface for working with a cache of jupyter notebooks. -This packages provides a clear [API](use/api) and [CLI](use/cli) for staging, executing and cacheing -Jupyter Notebooks. Although there are certainly other use cases, -the principle use case this was written for is generating books / websites, -created from multiple notebooks (and other text documents), -during which it is desired that notebooks can be *auto-executed* **only** -if the notebook had been modified in a way that may alter its code cell outputs. +This packages provides a clear [API](use/api) and [CLI](use/cli) for executing and cacheing multiple Jupyter Notebooks in a project. +Although there are certainly other use cases, the principle use case this was written for is generating books / websites, created from multiple notebooks (and other text documents). +It is desired that notebooks can be *auto-executed* **only** if the notebook had been modified in a way that may alter its code cell outputs. Some desired requirements (not yet all implemented): @@ -24,7 +21,7 @@ Some desired requirements (not yet all implemented): ## Installation -To install `jupytes-cache`, do the following: +To install `jupyter-cache`, do the following: ```bash pip install jupyter-cache[cli] @@ -42,10 +39,6 @@ pip install -e .[cli,code_style,testing,rtd] Here are the site contents: ```{toctree} ---- -maxdepth: 2 -caption: Contents ---- using/cli using/api develop/contributing diff --git a/docs/using/api.ipynb b/docs/using/api.ipynb index 3b4a69f..215da4f 100644 --- a/docs/using/api.ipynb +++ b/docs/using/api.ipynb @@ -2,50 +2,48 @@ "cells": [ { "cell_type": "markdown", - "metadata": {}, "source": [ "(use/api)=\n", "\n", "# Python API" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "This page outlines how to utilise the cache programatically.\n", "We step throught the three aspects illustrated in the diagram below:\n", - "[cacheing](use/api/cache), [staging](use/api/stage) and [executing](use/api/execute).\n", + "[cacheing](use/api/cache), [staging](use/api/project) and [executing](use/api/execute).\n", "\n", "```{figure} images/execution_process.svg\n", ":width: 500 px\n", "\n", "Illustration of the execution process.\n", "```" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "```{note}\n", - "The full Jupyter notebook for this page can accessed here; {nb-download:notebook.ipynb}`api`.\n", + "The full Jupyter notebook for this page can accessed here; {nb-download}`api.ipynb`.\n", "Try it for yourself!\n", "```" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "## Initialisation" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 1, - "metadata": {}, - "outputs": [], "source": [ "from pathlib import Path\n", "import nbformat as nbf\n", @@ -54,104 +52,105 @@ "from jupyter_cache.executors import load_executor, list_executors\n", "from jupyter_cache.utils import (\n", " tabulate_cache_records, \n", - " tabulate_stage_records\n", + " tabulate_project_records\n", ")" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "First we setup a cache and ensure that it is cleared.\n", "\n", "```{important}\n", "Clearing a cache wipes its entire content, including any settings (such as cache limit).\n", "```" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "source": [ + "cache = get_cache(\".jupyter_cache\")\n", + "cache.clear_cache()\n", + "cache" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "JupyterCacheBase('/Users/cjs14/GitHub/jupyter-cache/docs/using/.jupyter_cache')" ] }, - "execution_count": 2, "metadata": {}, - "output_type": "execute_result" + "execution_count": 2 } ], - "source": [ - "cache = get_cache(\".jupyter_cache\")\n", - "cache.clear_cache()\n", - "cache" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "source": [ + "print(cache.list_cache_records())\n", + "print(cache.nb_project_records())" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "[]\n", "[]\n" ] } ], - "source": [ - "print(cache.list_cache_records())\n", - "print(cache.list_staged_records())" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "(use/api/cache)=\n", "\n", "## Cacheing Notebooks" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "To directly cache a notebook:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 4, - "metadata": {}, + "source": [ + "record = cache.cache_notebook_file(\n", + " path=Path(\"example_nbs\", \"basic.ipynb\")\n", + ")\n", + "record" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, - "execution_count": 4, "metadata": {}, - "output_type": "execute_result" + "execution_count": 4 } ], - "source": [ - "record = cache.cache_notebook_file(\n", - " path=Path(\"example_nbs\", \"basic.ipynb\")\n", - ")\n", - "record" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "This will add a physical copy of the notebook to tha cache (stripped of any text cells) and return the record that has been added to the cache database.\n", "\n", @@ -160,14 +159,18 @@ "```\n", "\n", "The record stores metadata for the notebook:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "source": [ + "record.to_dict()" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "{'data': {},\n", @@ -179,105 +182,105 @@ " 'created': datetime.datetime(2020, 3, 13, 14, 21, 46, 271943)}" ] }, - "execution_count": 5, "metadata": {}, - "output_type": "execute_result" + "execution_count": 5 } ], - "source": [ - "record.to_dict()" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "```{important}\n", "The URI that the notebook is read from is stored, but does not have an impact on later comparison of notebooks. They are only compared by their internal content.\n", "```" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "We can retrive cache records by their Primary Key (pk): " - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 6, - "metadata": {}, + "source": [ + "cache.list_cache_records()" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "[NbCacheRecord(pk=1)]" ] }, - "execution_count": 6, "metadata": {}, - "output_type": "execute_result" + "execution_count": 6 } ], - "source": [ - "cache.list_cache_records()" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 7, - "metadata": {}, + "source": [ + "cache.get_cache_record(1)" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, - "execution_count": 7, "metadata": {}, - "output_type": "execute_result" + "execution_count": 7 } ], - "source": [ - "cache.get_cache_record(1)" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "To load the entire notebook that is related to a pk:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 8, - "metadata": {}, + "source": [ + "nb_bundle = cache.get_cache_bundle(1)\n", + "nb_bundle" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "NbBundleOut(nb=Notebook(cells=1), record=NbCacheRecord(pk=1), artifacts=NbArtifacts(paths=0))" ] }, - "execution_count": 8, "metadata": {}, - "output_type": "execute_result" + "execution_count": 8 } ], - "source": [ - "nb_bundle = cache.get_cache_bundle(1)\n", - "nb_bundle" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 9, - "metadata": {}, + "source": [ + "nb_bundle.nb" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "{'cells': [{'cell_type': 'code',\n", @@ -300,35 +303,32 @@ " 'nbformat_minor': 2}" ] }, - "execution_count": 9, "metadata": {}, - "output_type": "execute_result" + "execution_count": 9 } ], - "source": [ - "nb_bundle.nb" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "Trying to add a notebook to the cache that matches an existing one will result in a error, since the cache ensures that all notebook hashes are unique:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 10, - "metadata": { - "tags": [ - "raises-exception" - ] - }, + "source": [ + "record = cache.cache_notebook_file(\n", + " path=Path(\"example_nbs\", \"basic.ipynb\")\n", + ")" + ], "outputs": [ { + "output_type": "error", "ename": "CachingError", "evalue": "Notebook already exists in cache and overwrite=False.", - "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mCachingError\u001b[0m Traceback (most recent call last)", @@ -339,26 +339,30 @@ ] } ], - "source": [ - "record = cache.cache_notebook_file(\n", - " path=Path(\"example_nbs\", \"basic.ipynb\")\n", - ")" - ] + "metadata": { + "tags": [ + "raises-exception" + ] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "If we load a notebook external to the cache, then we can try to match it to one\n", "stored inside the cache:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "source": [ + "notebook = nbf.read(str(Path(\"example_nbs\", \"basic.ipynb\")), 4)\n", + "notebook" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "{'cells': [{'cell_type': 'markdown',\n", @@ -384,101 +388,95 @@ " 'nbformat_minor': 2}" ] }, - "execution_count": 11, "metadata": {}, - "output_type": "execute_result" + "execution_count": 11 } ], - "source": [ - "notebook = nbf.read(str(Path(\"example_nbs\", \"basic.ipynb\")), 4)\n", - "notebook" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 12, - "metadata": {}, + "source": [ + "cache.match_cache_notebook(notebook)" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, - "execution_count": 12, "metadata": {}, - "output_type": "execute_result" + "execution_count": 12 } ], - "source": [ - "cache.match_cache_notebook(notebook)" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "Notebooks are matched by a hash based only on aspects of the notebook that will affect its execution (and hence outputs). So changing text cells will match the cached notebook:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 13, - "metadata": {}, - "outputs": [], "source": [ "notebook.cells[0].source = \"change some text\"" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 14, - "metadata": {}, + "source": [ + "cache.match_cache_notebook(notebook)" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, - "execution_count": 14, "metadata": {}, - "output_type": "execute_result" + "execution_count": 14 } ], - "source": [ - "cache.match_cache_notebook(notebook)" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "But changing code cells will result in a different hash, and so will not be matched:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 15, - "metadata": {}, - "outputs": [], "source": [ "notebook.cells[1].source = \"change some source code\"" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 16, - "metadata": { - "tags": [ - "raises-exception" - ] - }, + "source": [ + "cache.match_cache_notebook(notebook)" + ], "outputs": [ { + "output_type": "error", "ename": "KeyError", "evalue": "'Cache record not found for NB with hashkey: 74933d8a93d1df9caad87b2e6efcdc69'", - "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", @@ -489,25 +487,29 @@ ] } ], - "source": [ - "cache.match_cache_notebook(notebook)" - ] + "metadata": { + "tags": [ + "raises-exception" + ] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "To understand the difference between an external notebook, and one stored in the cache, we can 'diff' them:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 17, - "metadata": {}, + "source": [ + "print(cache.diff_nbnode_with_cache(1, notebook, as_str=True))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "nbdiff\n", "--- cached pk=1\n", @@ -541,50 +543,52 @@ ] } ], - "source": [ - "print(cache.diff_nbnode_with_cache(1, notebook, as_str=True))" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "If we cache this altered notebook, note that this will not remove the previously cached notebook:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 18, - "metadata": {}, + "source": [ + "nb_bundle = NbBundleIn(\n", + " nb=notebook,\n", + " uri=Path(\"example_nbs\", \"basic.ipynb\"),\n", + " data={\"tag\": \"mytag\"}\n", + ")\n", + "cache.cache_notebook_bundle(nb_bundle)" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=2)" ] }, - "execution_count": 18, "metadata": {}, - "output_type": "execute_result" + "execution_count": 18 } ], - "source": [ - "nb_bundle = NbBundleIn(\n", - " nb=notebook,\n", - " uri=Path(\"example_nbs\", \"basic.ipynb\"),\n", - " data={\"tag\": \"mytag\"}\n", - ")\n", - "cache.cache_notebook_bundle(nb_bundle)" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 19, - "metadata": {}, + "source": [ + "print(tabulate_cache_records(\n", + " cache.list_cache_records(), path_length=1, hashkeys=True\n", + "))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " ID Origin URI Created Accessed Hashkey\n", "---- ------------ ---------------- ---------------- --------------------------------\n", @@ -593,61 +597,56 @@ ] } ], - "source": [ - "print(tabulate_cache_records(\n", - " cache.list_cache_records(), path_length=1, hashkeys=True\n", - "))" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "Notebooks are retained in the cache, until the cache limit is reached,\n", "at which point the oldest notebooks are removed." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 20, - "metadata": {}, + "source": [ + "cache.get_cache_limit()" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "1000" ] }, - "execution_count": 20, "metadata": {}, - "output_type": "execute_result" + "execution_count": 20 } ], - "source": [ - "cache.get_cache_limit()" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 21, - "metadata": {}, - "outputs": [], "source": [ "cache.change_cache_limit(100)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ - "(use/api/stage)=\n", + "(use/api/project)=\n", "\n", "## Staging Notebooks for Execution" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "Notebooks can be staged, by adding the path as a stage record.\n", "\n", @@ -655,35 +654,39 @@ "This does not physically add the notebook to the cache,\n", "merely store its URI, for later use.\n", "```" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 22, - "metadata": {}, + "source": [ + "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", + "record" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ - "NbStageRecord(pk=1)" + "NbProjectRecord(pk=1)" ] }, - "execution_count": 22, "metadata": {}, - "output_type": "execute_result" + "execution_count": 22 } ], - "source": [ - "record = cache.stage_notebook_file(Path(\"example_nbs\", \"basic.ipynb\"))\n", - "record" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 23, - "metadata": {}, + "source": [ + "record.to_dict()" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "{'uri': '/Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb',\n", @@ -693,50 +696,51 @@ " 'pk': 1}" ] }, - "execution_count": 23, "metadata": {}, - "output_type": "execute_result" + "execution_count": 23 } ], - "source": [ - "record.to_dict()" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "If the staged notbook relates to one in the cache, we will be able to retrieve the cache record:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 24, - "metadata": {}, + "source": [ + "cache.get_cached_project_nb(1)" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, - "execution_count": 24, "metadata": {}, - "output_type": "execute_result" + "execution_count": 24 } ], - "source": [ - "cache.get_cache_record_of_staged(1)" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 25, - "metadata": {}, + "source": [ + "print(tabulate_project_records(\n", + " cache.nb_project_records(), path_length=2, cache=cache\n", + "))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " ID URI Created Assets Cache ID\n", "---- ----------------------- ---------------- -------- ----------\n", @@ -744,15 +748,10 @@ ] } ], - "source": [ - "print(tabulate_stage_records(\n", - " cache.list_staged_records(), path_length=2, cache=cache\n", - "))" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "We can also retrieve a *merged* notebook.\n", "This is a copy of the source notebook with the following added to it from the cached notebook:\n", @@ -762,14 +761,22 @@ " (only selected metadata can be merged if `cell_meta` is not `None`)\n", " \n", "In this way we create a notebook that is *fully* up-to-date for both its code and textual content:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 26, - "metadata": {}, + "source": [ + "cache.merge_match_into_file(\n", + " cache.get_project_record(1).uri,\n", + " nb_meta=('kernelspec', 'language_info', 'widgets'),\n", + " cell_meta=None\n", + ")" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "(1,\n", @@ -796,84 +803,81 @@ " 'nbformat_minor': 2})" ] }, - "execution_count": 26, "metadata": {}, - "output_type": "execute_result" + "execution_count": 26 } ], - "source": [ - "cache.merge_match_into_file(\n", - " cache.get_staged_record(1).uri,\n", - " nb_meta=('kernelspec', 'language_info', 'widgets'),\n", - " cell_meta=None\n", - ")" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "If we add a notebook that cannot be found in the cache, it will be listed for execution:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 27, - "metadata": {}, + "source": [ + "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic_failing.ipynb\"))\n", + "record" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ - "NbStageRecord(pk=2)" + "NbProjectRecord(pk=2)" ] }, - "execution_count": 27, "metadata": {}, - "output_type": "execute_result" + "execution_count": 27 } ], - "source": [ - "record = cache.stage_notebook_file(Path(\"example_nbs\", \"basic_failing.ipynb\"))\n", - "record" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 28, - "metadata": {}, - "outputs": [], "source": [ - "cache.get_cache_record_of_staged(2) # returns None" - ] + "cache.get_cached_project_nb(2) # returns None" + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 29, - "metadata": {}, + "source": [ + "cache.list_unexecuted()" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ - "[NbStageRecord(pk=2)]" + "[NbProjectRecord(pk=2)]" ] }, - "execution_count": 29, "metadata": {}, - "output_type": "execute_result" + "execution_count": 29 } ], - "source": [ - "cache.list_staged_unexecuted()" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 30, - "metadata": {}, + "source": [ + "print(tabulate_project_records(\n", + " cache.nb_project_records(), path_length=2, cache=cache\n", + "))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " ID URI Created Assets Cache ID\n", "---- ------------------------------- ---------------- -------- ----------\n", @@ -882,36 +886,36 @@ ] } ], - "source": [ - "print(tabulate_stage_records(\n", - " cache.list_staged_records(), path_length=2, cache=cache\n", - "))" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "To remove a notebook from the staging area:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 31, - "metadata": {}, - "outputs": [], "source": [ - "cache.discard_staged_notebook(1)" - ] + "cache.remove_nb_from_project(1)" + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 32, - "metadata": {}, + "source": [ + "print(tabulate_project_records(\n", + " cache.nb_project_records(), path_length=2, cache=cache\n", + "))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " ID URI Created Assets\n", "---- ------------------------------- ---------------- --------\n", @@ -919,58 +923,58 @@ ] } ], - "source": [ - "print(tabulate_stage_records(\n", - " cache.list_staged_records(), path_length=2, cache=cache\n", - "))" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "(use/api/execute)=\n", "\n", "## Execution" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "If we have some staged notebooks:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 33, - "metadata": {}, + "source": [ + "cache.clear_cache()\n", + "cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", + "cache.add_nb_to_project(Path(\"example_nbs\", \"basic_failing.ipynb\"))" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ - "NbStageRecord(pk=2)" + "NbProjectRecord(pk=2)" ] }, - "execution_count": 33, "metadata": {}, - "output_type": "execute_result" + "execution_count": 33 } ], - "source": [ - "cache.clear_cache()\n", - "cache.stage_notebook_file(Path(\"example_nbs\", \"basic.ipynb\"))\n", - "cache.stage_notebook_file(Path(\"example_nbs\", \"basic_failing.ipynb\"))" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 34, - "metadata": {}, + "source": [ + "print(tabulate_project_records(\n", + " cache.nb_project_records(), path_length=2, cache=cache\n", + "))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " ID URI Created Assets\n", "---- ------------------------------- ---------------- --------\n", @@ -979,15 +983,10 @@ ] } ], - "source": [ - "print(tabulate_stage_records(\n", - " cache.list_staged_records(), path_length=2, cache=cache\n", - "))" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "Then we can select an executor (specified as entry points) to execute the notebook.\n", "\n", @@ -995,55 +994,55 @@ "To view the executors log, make sure logging is enabled,\n", "or you can parse a logger directly to `load_executor()`.\n", "```" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 35, - "metadata": {}, + "source": [ + "list_executors()" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "[EntryPoint.parse('basic = jupyter_cache.executors.basic:JupyterExecutorBasic')]" ] }, - "execution_count": 35, "metadata": {}, - "output_type": "execute_result" + "execution_count": 35 } ], - "source": [ - "list_executors()" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 36, - "metadata": {}, + "source": [ + "from logging import basicConfig, INFO\n", + "basicConfig(level=INFO)\n", + "\n", + "executor = load_executor(\"basic\", cache=cache)\n", + "executor" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "JupyterExecutorBasic(cache=JupyterCacheBase('/Users/cjs14/GitHub/jupyter-cache/docs/using/.jupyter_cache'))" ] }, - "execution_count": 36, "metadata": {}, - "output_type": "execute_result" + "execution_count": 36 } ], - "source": [ - "from logging import basicConfig, INFO\n", - "basicConfig(level=INFO)\n", - "\n", - "executor = load_executor(\"basic\", cache=cache)\n", - "executor" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "Calling `run_and_cache()` will run all staged notebooks that do not already have matches in the cache.\n", "It will return a dictionary with lists for:\n", @@ -1062,16 +1061,20 @@ "You can use the `filter_uris` and/or `filter_pks` options to only run selected staged notebooks.\n", "You can also specify the timeout for execution in seconds using the `timeout` option.\n", "```" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 37, - "metadata": {}, + "source": [ + "result = executor.run_and_cache()\n", + "result" + ], "outputs": [ { - "name": "stderr", "output_type": "stream", + "name": "stderr", "text": [ "INFO:jupyter_cache.executors.base:Executing: /Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb\n", "INFO:jupyter_cache.executors.base:Execution Succeeded: /Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb\n", @@ -1080,6 +1083,7 @@ ] }, { + "output_type": "execute_result", "data": { "text/plain": [ "{'succeeded': ['/Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb'],\n", @@ -1087,49 +1091,49 @@ " 'errored': []}" ] }, - "execution_count": 37, "metadata": {}, - "output_type": "execute_result" + "execution_count": 37 } ], - "source": [ - "result = executor.run_and_cache()\n", - "result" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "Successfully executed notebooks will be added to the cache, and data about their execution (such as time taken) will be stored in the cache record:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 38, - "metadata": {}, + "source": [ + "cache.list_cache_records()" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "[NbCacheRecord(pk=1)]" ] }, - "execution_count": 38, "metadata": {}, - "output_type": "execute_result" + "execution_count": 38 } ], - "source": [ - "cache.list_cache_records()" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 39, - "metadata": {}, + "source": [ + "record = cache.get_cache_record(1)\n", + "record.to_dict()" + ], "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ "{'data': {'execution_seconds': 1.7455324890000004},\n", @@ -1141,33 +1145,32 @@ " 'created': datetime.datetime(2020, 3, 13, 14, 21, 50, 803031)}" ] }, - "execution_count": 39, "metadata": {}, - "output_type": "execute_result" + "execution_count": 39 } ], - "source": [ - "record = cache.get_cache_record(1)\n", - "record.to_dict()" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "Notebooks which failed to run will **not** be added to the cache,\n", "but details about their execution (including the exception traceback)\n", "will be added to the stage record:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 40, - "metadata": {}, + "source": [ + "record = cache.get_project_record(2)\n", + "print(record.traceback)" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "Traceback (most recent call last):\n", " File \"/Users/cjs14/GitHub/jupyter-cache/jupyter_cache/executors/basic.py\", line 152, in execute\n", @@ -1197,26 +1200,27 @@ ] } ], - "source": [ - "record = cache.get_staged_record(2)\n", - "print(record.traceback)" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "We now have two staged records, and one cache record:" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 41, - "metadata": {}, + "source": [ + "print(tabulate_project_records(\n", + " cache.nb_project_records(), path_length=2, cache=cache\n", + "))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " ID URI Created Assets Cache ID\n", "---- ------------------------------- ---------------- -------- ----------\n", @@ -1225,20 +1229,20 @@ ] } ], - "source": [ - "print(tabulate_stage_records(\n", - " cache.list_staged_records(), path_length=2, cache=cache\n", - "))" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 42, - "metadata": {}, + "source": [ + "print(tabulate_cache_records(\n", + " cache.list_cache_records(), path_length=1, hashkeys=True\n", + "))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ " ID Origin URI Created Accessed Hashkey\n", "---- ------------ ---------------- ---------------- --------------------------------\n", @@ -1246,15 +1250,10 @@ ] } ], - "source": [ - "print(tabulate_cache_records(\n", - " cache.list_cache_records(), path_length=1, hashkeys=True\n", - "))" - ] + "metadata": {} }, { "cell_type": "markdown", - "metadata": {}, "source": [ "### Timeout\n", "A **timeout** argument can also be passed to `run_and_cache()` which takes value in seconds.\n", @@ -1268,7 +1267,8 @@ "```{note}\n", "Timeout specified in notebook metadata will take precedence over the one passed as an argument to `run_and_cache()`.\n", "```" - ] + ], + "metadata": {} } ], "metadata": { diff --git a/docs/using/cli.md b/docs/using/cli.md index 669d2ba..6624fc9 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -2,7 +2,7 @@ # Command-Line - + From the checked-out repository folder: @@ -22,8 +22,8 @@ Commands: cache Commands for adding to and inspecting the cache. clear Clear the cache completely. config Commands for configuring the cache. - execute Execute notebooks that are not in the cache or outdated. - stage Commands for staging notebooks to be executed. + execute Execute all or specific outdated notebooks in the project. + project Commands for interacting with a project. ``` ````{tip} @@ -49,6 +49,7 @@ Commands: add Cache notebook(s) that have already been executed. add-with-artefacts Cache a notebook, with possible artefact files. cat-artifact Print the contents of a cached artefact. + clear Remove all executed notebooks from the cache. diff-nb Print a diff of a notebook to one stored in the cache. list List cached notebook records in the cache. remove Remove notebooks stored in the cache. @@ -93,7 +94,7 @@ Once you've cached some notebooks, you can look at the 'cache records' for what has been cached. Each notebook is hashed (code cells and kernel spec only), -which is used to compare against 'staged' notebooks. +which is used to compare against notebooks in the project. Multiple hashes for the same URI can be added (the URI is just there for inspection) and the size of the cache is limited (current default 1000) so that, at this size, @@ -104,10 +105,10 @@ You can remove cached records by their ID. $ jcache cache list ID Origin URI Created Accessed ---- ------------------------------------- ---------------- ---------------- - 5 tests/notebooks/external_output.ipynb 2021-08-01 15:43 2021-08-01 15:43 - 4 tests/notebooks/complex_outputs.ipynb 2021-08-01 15:43 2021-08-01 15:43 - 3 tests/notebooks/basic_unrun.ipynb 2021-08-01 15:43 2021-08-01 15:43 - 2 tests/notebooks/basic_failing.ipynb 2021-08-01 15:43 2021-08-01 15:43 + 5 tests/notebooks/external_output.ipynb 2021-08-02 01:51 2021-08-02 01:51 + 4 tests/notebooks/complex_outputs.ipynb 2021-08-02 01:51 2021-08-02 01:51 + 3 tests/notebooks/basic_unrun.ipynb 2021-08-02 01:51 2021-08-02 01:51 + 2 tests/notebooks/basic_failing.ipynb 2021-08-02 01:51 2021-08-02 01:51 ``` ````{tip} @@ -135,8 +136,8 @@ Show a full description of a cached notebook by referring to its ID $ jcache cache show 6 ID: 6 Origin URI: ../tests/notebooks/basic.ipynb -Created: 2021-08-01 15:43 -Accessed: 2021-08-01 15:43 +Created: 2021-08-02 01:51 +Accessed: 2021-08-02 01:51 Hashkey: 94c17138f782c75df59e989fffa64e3a Artifacts: - artifact_folder/artifact.txt @@ -195,59 +196,60 @@ nbdiff Success! ``` -## Staging Notebooks for execution +## Adding notebooks to the project ```console -$ jcache stage --help -Usage: stage [OPTIONS] COMMAND [ARGS]... +$ jcache project --help +Usage: project [OPTIONS] COMMAND [ARGS]... - Commands for staging notebooks to be executed. + Commands for creating/interacting with a project of notebooks. Options: --help Show this message and exit. Commands: - add Stage notebook(s) for execution. - add-with-assets Stage a notebook, with possible asset files. - list List notebooks staged for possible execution. - remove-ids Un-stage notebook(s), by ID. - remove-uris Un-stage notebook(s), by URI. - show Show details of a staged notebook. + add Add notebook(s) to the project. + add-with-assets Add notebook(s) to the project, with possible asset files. + list List notebooks in the project. + remove-ids Remove notebook(s) from the project, by ID. + remove-uris Remove notebook(s) from the project, by URI. + show Show details of a notebook. ``` -Staged notebooks are recorded as pointers to their URI, +A project consist of a set of notebooks to be executed. + +Notebooks are recorded as pointers to their URI (e.g. file path), i.e. no physical copying takes place until execution time. -If you stage some notebooks for execution, then -you can list them to see which have existing records in the cache (by hash), +You can list the notebooks to see which have existing records in the cache (by hash), and which will require execution: ```console -$ jcache stage add tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb -Staging: ../tests/notebooks/basic.ipynb -Staging: ../tests/notebooks/basic_failing.ipynb -Staging: ../tests/notebooks/basic_unrun.ipynb -Staging: ../tests/notebooks/complex_outputs.ipynb -Staging: ../tests/notebooks/external_output.ipynb +$ jcache project add tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb +Adding: ../tests/notebooks/basic.ipynb +Adding: ../tests/notebooks/basic_failing.ipynb +Adding: ../tests/notebooks/basic_unrun.ipynb +Adding: ../tests/notebooks/complex_outputs.ipynb +Adding: ../tests/notebooks/external_output.ipynb Success! ``` ```console -$ jcache stage list +$ jcache project list ID URI Reader Created Assets Cache ID ---- ------------------------------------- -------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb nbformat 2021-08-01 15:43 0 5 - 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-01 15:43 0 - 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-01 15:43 0 6 - 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-01 15:43 0 2 - 1 tests/notebooks/basic.ipynb nbformat 2021-08-01 15:43 0 6 + 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 01:51 0 5 + 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-02 01:51 0 + 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 01:51 0 6 + 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 01:51 0 2 + 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 01:51 0 6 ``` -You can remove a staged notebook by its URI or ID: +You can remove a notebook from the project by its URI or ID: ```console -$ jcache stage remove-ids 4 -Unstaging ID: 4 +$ jcache project remove-ids 4 +Removing: 4 Success! ``` @@ -284,13 +286,13 @@ along with any 'artefacts' created by the execution, that are inside the notebook folder, and data supplied by the executor. ```console -$ jcache stage list +$ jcache project list ID URI Reader Created Assets Cache ID ---- ------------------------------------- -------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb nbformat 2021-08-01 15:43 0 5 - 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-01 15:43 0 6 - 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-01 15:43 0 - 1 tests/notebooks/basic.ipynb nbformat 2021-08-01 15:43 0 6 + 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 01:51 0 5 + 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 01:51 0 6 + 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 01:51 0 + 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 01:51 0 6 ``` Execution data (such as execution time) will be stored in the cache record: @@ -299,22 +301,22 @@ Execution data (such as execution time) will be stored in the cache record: $ jcache cache show 6 ID: 6 Origin URI: ../tests/notebooks/basic_unrun.ipynb -Created: 2021-08-01 15:43 -Accessed: 2021-08-01 15:43 +Created: 2021-08-02 01:51 +Accessed: 2021-08-02 01:51 Hashkey: 94c17138f782c75df59e989fffa64e3a Data: - execution_seconds: 0.9876813249999996 + execution_seconds: 2.027238027 ``` -Failed notebooks will not be cached, but the exception traceback will be added to the stage record: +Failed notebooks will not be cached, but the exception traceback will be added to the notebook's project record: ```console -$ jcache stage show 2 +$ jcache project show 2 ID: 2 URI: ../tests/notebooks/basic_failing.ipynb Reader: nbformat -Created: 2021-08-01 15:43 +Created: 2021-08-02 01:51 Failed Last Execution! Traceback (most recent call last): File "../jupyter_cache/executors/utils.py", line 55, in single_nb_execution @@ -340,7 +342,7 @@ raise Exception('oopsie!') --------------------------------------------------------------------------- Exception Traceback (most recent call last) -/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_39409/340246212.py in +/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_76025/340246212.py in ----> 1 raise Exception('oopsie!') Exception: oopsie! @@ -354,34 +356,34 @@ Code cells can be tagged with `raises-exception` to let the executor known that (see [this issue on its behaviour](https://github.com/jupyter/nbconvert/issues/730)). ``` -Once executed you may leave staged notebooks, for later re-execution, or remove them: +Once executed you may leave notebooks in the project, for later re-execution, or remove them: ```console -$ jcache stage remove-ids --all +$ jcache project remove-ids --all Are you sure you want to remove all? [y/N]: y -Unstaging ID: 1 -Unstaging ID: 2 -Unstaging ID: 3 -Unstaging ID: 5 +Removing: 1 +Removing: 2 +Removing: 3 +Removing: 5 Success! ``` -You can also stage notebooks with assets; +You can also add notebooks to the projects with assets; external files that are required by the notebook during execution. As with artefacts, these files must be in the same folder as the notebook, or a sub-folder. ```console -$ jcache stage add-with-assets -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt +$ jcache project add-with-assets -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt Success! ``` ```console -$ jcache stage show 1 +$ jcache project show 1 ID: 1 URI: ../tests/notebooks/basic.ipynb Reader: nbformat -Created: 2021-08-01 15:43 +Created: 2021-08-02 01:51 Cache ID: 6 Assets: - ../tests/notebooks/artifact_folder/artifact.txt diff --git a/docs/using/images/execution_flow.pptx b/docs/using/images/execution_flow.pptx index 4d6108f2c7189cb691d8c019429e7fa627640bd0..f4aad67fdcd0066a0a9738dbc5653f169e760dad 100644 GIT binary patch delta 23117 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 delta 25876 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 U)`j3}CnrQ^10}w7!(Wd70)EPbdjJ3c diff --git a/docs/using/images/execution_process.svg b/docs/using/images/execution_process.svg index 680c219..31fa770 100644 --- a/docs/using/images/execution_process.svg +++ b/docs/using/images/execution_process.svg @@ -1,743 +1,969 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +PROJECT FOLDER + + + + + + + + + + + + - - - - - - - - - - - - - - - - +Notebook 1 + + + + + + + + + + + + - - - - - - - - - - - - - - - - - +Notebook 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + +EXECUTION +1. +Get notebook path from +database +2. +Get actual notebook from +project folder +3. +Check if notebook already +exists in the cache ( +via +hash) +4. +If not, read notebook and +execute +5. +If successful, write executed +notebook to the cache. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + +CACHE FOLDER + + + + + + + + + + + + + + + + + + + + + + +COMMITTED NOTEBOOKS + + + + + + + + + + + + + + + + + + + + + +Notebook A + + + + + + + + + + + + + + + + + + + + + +Notebook B + + + + + + + + + + + + + + + + + + + + + + + + + + + +PROJECT DATABASE + + + + + + + + + + + + + + + + + + + + + + + + + +EXECUTOR + + + + + + + + + + + + + + + + + +Notebook 1 Copy + + + + + + + + + + + + + + + + + + + + + + + +Notebook 1 Executed + + + + + + + + + + + + + + + +HASH + + + + + + + + +URI + + + diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index a6a69c9..3eebfce 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -13,7 +13,7 @@ from attr.validators import instance_of, optional # TODO make these abstract -from jupyter_cache.cache.db import NbCacheRecord, NbStageRecord +from jupyter_cache.cache.db import NbCacheRecord, NbProjectRecord NB_VERSION = 4 @@ -242,10 +242,10 @@ def diff_nbfile_with_cache( return self.diff_nbnode_with_cache(pk, nb, uri=path, as_str=as_str, **kwargs) @abstractmethod - def stage_notebook_file( - self, uri: str, *, reader: Optional[str] = None, assets: List[str] = () - ) -> NbStageRecord: - """Stage a single notebook for execution. + def add_nb_to_project( + self, uri: str, *, reader: str = "nbformat", assets: List[str] = () + ) -> NbProjectRecord: + """Add a single notebook to the project. :param uri: The path to the file :param reader: A key for the reader function, to read the uri and return a NotebookNode @@ -254,37 +254,30 @@ def stage_notebook_file( """ @abstractmethod - def discard_staged_notebook(self, uri_or_pk: Union[int, str]): - """Discard a staged notebook.""" + def remove_nb_from_project(self, uri_or_pk: Union[int, str]): + """Remove a notebook from the project.""" @abstractmethod - def list_staged_records(self) -> List[NbStageRecord]: - """list staged notebook URI's in the cache.""" + def nb_project_records(self) -> List[NbProjectRecord]: + """Return a list of the notebook URI's in the project.""" @abstractmethod - def get_staged_record(self, uri_or_pk: Union[int, str]) -> NbStageRecord: - """Return the record of a staged notebook, by its primary key or URI.""" + def get_project_record(self, uri_or_pk: Union[int, str]) -> NbProjectRecord: + """Return the record of a notebook in the project, by its primary key or URI.""" @abstractmethod - def get_staged_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: - """Return a single staged notebook, by its primary key or URI.""" + def get_project_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: + """Return a single notebook in the project, by its primary key or URI.""" @abstractmethod - def get_cache_record_of_staged( + def get_cached_project_nb( self, uri_or_pk: Union[int, str] ) -> Optional[NbCacheRecord]: - """Get cache record from staged notebook.""" + """Get cache record for a notebook in the project. + + :param uri_or_pk: The URI of pk of the file in the project + """ @abstractmethod - def list_staged_unexecuted(self) -> List[NbStageRecord]: - """List staged notebooks, whose hash is not present in the cache.""" - - # removed until defined use case - # @abstractmethod - # def get_cache_codecell(self, pk: int, index: int) -> nbf.NotebookNode: - # """Return a code cell from a cached notebook. - - # NOTE: the index **only** refers to the list of code cells, e.g. - # `[codecell_0, textcell_1, codecell_2]` - # would map {0: codecell_0, 1: codecell_2} - # """ + def list_unexecuted(self) -> List[NbProjectRecord]: + """List notebooks in the project, whose hash is not present in the cache.""" diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index a7b179b..1cd1272 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -17,7 +17,7 @@ # TODO store this in the database so we can check for updates DB_VERSION = 2 -# v2 added reader field to nbstage +# v2: nbstage -> nbproject, and added reader field to nbproject def create_db(path, name="global.db") -> Engine: @@ -226,10 +226,10 @@ def records_to_delete(keep: int, db: Engine) -> List[int]: return pks_to_delete -class NbStageRecord(OrmBase): - """A record of a notebook staged for execution.""" +class NbProjectRecord(OrmBase): + """A record of a notebook within the project.""" - __tablename__ = "nbstage" + __tablename__ = "nbproject" pk = Column(Integer(), primary_key=True) uri = Column(String(255), nullable=False, unique=True) @@ -287,76 +287,76 @@ def create_record( *, reader: str = "nbformat", assets=(), - ) -> "NbStageRecord": - assets = NbStageRecord.validate_assets(assets, uri) + ) -> "NbProjectRecord": + assets = NbProjectRecord.validate_assets(assets, uri) with session_context(db) as session: # type: Session - record = NbStageRecord(uri=uri, reader=reader, assets=assets) + record = NbProjectRecord(uri=uri, reader=reader, assets=assets) session.add(record) try: session.commit() except IntegrityError: if raise_on_exists: - raise ValueError(f"uri already staged: {uri}") - return NbStageRecord.record_from_uri(uri, db) + raise ValueError(f"URI already in project: {uri}") + return NbProjectRecord.record_from_uri(uri, db) session.refresh(record) session.expunge(record) return record def remove_pks(pks: List[int], db: Engine): with session_context(db) as session: # type: Session - session.query(NbStageRecord).filter(NbStageRecord.pk.in_(pks)).delete( + session.query(NbProjectRecord).filter(NbProjectRecord.pk.in_(pks)).delete( synchronize_session=False ) session.commit() def remove_uris(uris: List[str], db: Engine): with session_context(db) as session: # type: Session - session.query(NbStageRecord).filter(NbStageRecord.uri.in_(uris)).delete( + session.query(NbProjectRecord).filter(NbProjectRecord.uri.in_(uris)).delete( synchronize_session=False ) session.commit() @staticmethod - def record_from_pk(pk: int, db: Engine) -> "NbStageRecord": + def record_from_pk(pk: int, db: Engine) -> "NbProjectRecord": with session_context(db) as session: # type: Session - result = session.query(NbStageRecord).filter_by(pk=pk).one_or_none() + result = session.query(NbProjectRecord).filter_by(pk=pk).one_or_none() if result is None: - raise KeyError("Staging record not found for NB with PK: {}".format(pk)) + raise KeyError("Project record not found for NB with PK: {}".format(pk)) session.expunge(result) return result @staticmethod - def record_from_uri(uri: str, db: Engine) -> "NbStageRecord": + def record_from_uri(uri: str, db: Engine) -> "NbProjectRecord": with session_context(db) as session: # type: Session - result = session.query(NbStageRecord).filter_by(uri=uri).one_or_none() + result = session.query(NbProjectRecord).filter_by(uri=uri).one_or_none() if result is None: raise KeyError( - "Staging record not found for NB with URI: {}".format(uri) + "Project record not found for NB with URI: {}".format(uri) ) session.expunge(result) return result @staticmethod - def records_all(db: Engine) -> "NbStageRecord": + def records_all(db: Engine) -> "NbProjectRecord": with session_context(db) as session: # type: Session - results = session.query(NbStageRecord).all() + results = session.query(NbProjectRecord).all() session.expunge_all() return results def remove_tracebacks(pks, db: Engine): """Remove all tracebacks.""" with session_context(db) as session: # type: Session - session.query(NbStageRecord).filter(NbStageRecord.pk.in_(pks)).update( - {NbStageRecord.traceback: None}, synchronize_session=False + session.query(NbProjectRecord).filter(NbProjectRecord.pk.in_(pks)).update( + {NbProjectRecord.traceback: None}, synchronize_session=False ) session.commit() def set_traceback(uri: str, traceback: Optional[str], db: Engine): with session_context(db) as session: # type: Session - result = session.query(NbStageRecord).filter_by(uri=uri).one_or_none() + result = session.query(NbProjectRecord).filter_by(uri=uri).one_or_none() if result is None: raise KeyError( - "Staging record not found for NB with URI: {}".format(uri) + "Project record not found for NB with URI: {}".format(uri) ) result.traceback = traceback try: diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 63c46cf..23b09e1 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -21,7 +21,7 @@ from jupyter_cache.readers import get_reader from jupyter_cache.utils import to_relative_paths -from .db import NbCacheRecord, NbStageRecord, Setting, create_db +from .db import NbCacheRecord, NbProjectRecord, Setting, create_db CACHE_LIMIT_KEY = "cache_limit" DEFAULT_CACHE_LIMIT = 1000 @@ -399,19 +399,13 @@ def diff_nbnode_with_cache( ) return stream.getvalue() - def stage_notebook_file( - self, path: str, *, reader: str = "nbformat", assets=() - ) -> NbStageRecord: - """Stage a single notebook for execution. - - :param uri: The path to the file - :param reader: A key for the reader function. - :param assets: The path of files required by the notebook to run. - These must be within the same folder as the notebook. - """ + def add_nb_to_project( + self, path: str, *, reader: str = "nbformat", assets: List[str] = () + ) -> NbProjectRecord: + # check the reader can be loaded _ = get_reader(reader) # TODO should we test that the file can be read by the reader? - return NbStageRecord.create_record( + return NbProjectRecord.create_record( str(Path(path).absolute()), self.db, raise_on_exists=False, @@ -421,58 +415,55 @@ def stage_notebook_file( # TODO physically copy to cache? # TODO assets - def list_staged_records(self) -> List[NbStageRecord]: - return NbStageRecord.records_all(self.db) + def nb_project_records(self) -> List[NbProjectRecord]: + return NbProjectRecord.records_all(self.db) - def get_staged_record(self, uri_or_pk: Union[int, str]) -> NbStageRecord: + def get_project_record(self, uri_or_pk: Union[int, str]) -> NbProjectRecord: if isinstance(uri_or_pk, int): - record = NbStageRecord.record_from_pk(uri_or_pk, self.db) + record = NbProjectRecord.record_from_pk(uri_or_pk, self.db) else: - record = NbStageRecord.record_from_uri(uri_or_pk, self.db) + record = NbProjectRecord.record_from_uri(uri_or_pk, self.db) return record - def discard_staged_notebook(self, uri_or_pk: Union[int, str]): - """Discard a staged notebook.""" + def remove_nb_from_project(self, uri_or_pk: Union[int, str]): if isinstance(uri_or_pk, int): - NbStageRecord.remove_pks([uri_or_pk], self.db) + NbProjectRecord.remove_pks([uri_or_pk], self.db) else: - NbStageRecord.remove_uris([uri_or_pk], self.db) + NbProjectRecord.remove_uris([uri_or_pk], self.db) - # TODO add discard all/multiple staged records method + # TODO add discard all/multiple project records method - def get_staged_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: - """Return a single staged notebook.""" + def get_project_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: if isinstance(uri_or_pk, int): - record = NbStageRecord.record_from_pk(uri_or_pk, self.db) + record = NbProjectRecord.record_from_pk(uri_or_pk, self.db) else: - record = NbStageRecord.record_from_uri(uri_or_pk, self.db) + record = NbProjectRecord.record_from_uri(uri_or_pk, self.db) if not Path(record.uri).exists(): raise IOError( - "The URI of the staged record no longer exists: {}".format(record.uri) + "The URI of the project record no longer exists: {}".format(record.uri) ) converter = get_reader(record.reader) notebook = converter(record.uri) return NbBundleIn(notebook, record.uri) - def get_cache_record_of_staged( + def get_cached_project_nb( self, uri_or_pk: Union[int, str] ) -> Optional[NbCacheRecord]: if isinstance(uri_or_pk, int): - record = NbStageRecord.record_from_pk(uri_or_pk, self.db) + record = NbProjectRecord.record_from_pk(uri_or_pk, self.db) else: - record = NbStageRecord.record_from_uri(uri_or_pk, self.db) - nb = self.get_staged_notebook(record.uri).nb + record = NbProjectRecord.record_from_uri(uri_or_pk, self.db) + nb = self.get_project_notebook(record.uri).nb _, hashkey = self.create_hashed_notebook(nb) try: return NbCacheRecord.record_from_hashkey(hashkey, self.db) except KeyError: return None - def list_staged_unexecuted(self) -> List[NbStageRecord]: - """List staged notebooks, whose hash is not present in the cached notebooks.""" + def list_unexecuted(self) -> List[NbProjectRecord]: records = [] - for record in self.list_staged_records(): - nb = self.get_staged_notebook(record.uri).nb + for record in self.nb_project_records(): + nb = self.get_project_notebook(record.uri).nb _, hashkey = self.create_hashed_notebook(nb) try: NbCacheRecord.record_from_hashkey(hashkey, self.db) diff --git a/jupyter_cache/cli/commands/__init__.py b/jupyter_cache/cli/commands/__init__.py index df7c551..ea0fe33 100644 --- a/jupyter_cache/cli/commands/__init__.py +++ b/jupyter_cache/cli/commands/__init__.py @@ -6,4 +6,4 @@ from .cmd_cache import * # noqa: F401,F403,E402 from .cmd_config import * # noqa: F401,F403,E402 from .cmd_exec import * # noqa: F401,F403,E402 -from .cmd_stage import * # noqa: F401,F403,E402 +from .cmd_project import * # noqa: F401,F403,E402 diff --git a/jupyter_cache/cli/commands/cmd_cache.py b/jupyter_cache/cli/commands/cmd_cache.py index a1d2b40..cd88162 100644 --- a/jupyter_cache/cli/commands/cmd_cache.py +++ b/jupyter_cache/cli/commands/cmd_cache.py @@ -155,6 +155,24 @@ def cache_nbs(cache_path, nbpaths, validate, overwrite): click.secho("Success!", fg="green") +@cmnd_cache.command("clear") +@options.CACHE_PATH +@options.FORCE +def clear_cache_cmd(cache_path, force): + """Remove all executed notebooks from the cache.""" + db = get_cache(cache_path) + if not force: + click.confirm( + "Are you sure you want to permanently clear the cache!?", abort=True + ) + for pk in db.list_cache_records(): + try: + db.remove_cache(pk) + except Exception: + pass + click.secho("Cache cleared!", fg="green") + + @cmnd_cache.command("remove") @arguments.PKS @options.CACHE_PATH diff --git a/jupyter_cache/cli/commands/cmd_exec.py b/jupyter_cache/cli/commands/cmd_exec.py index 9320672..311caf7 100644 --- a/jupyter_cache/cli/commands/cmd_exec.py +++ b/jupyter_cache/cli/commands/cmd_exec.py @@ -20,32 +20,31 @@ @options.CACHE_PATH @click_log.simple_verbosity_option(logger) def execute_nbs(cache_path, executor, pk_paths, timeout): - """Execute notebooks that are not in the cache or outdated.""" + """Execute all or specific outdated notebooks in the project.""" import yaml from jupyter_cache.executors import load_executor db = get_cache(cache_path) records = [] - unstaged = [] + not_in_project = [] for pk_path in pk_paths: if pk_path.isdigit(): pk_path = int(pk_path) - record = db.get_staged_record(int(pk_path)) + record = db.get_project_record(int(pk_path)) records.append(record) else: try: - record = db.get_staged_record(str(Path(pk_path).absolute())) + record = db.get_project_record(str(Path(pk_path).absolute())) records.append(record) except KeyError: if not Path(pk_path).exists(): raise FileNotFoundError(f"'{pk_path}' does not exist.") - unstaged.append(pk_path) - if unstaged: - # ask to stage, select reader - unstaged_string = "\n - ".join(unstaged) - click.echo(f"Unstaged notebooks specified:\n - {unstaged_string}") - if not click.confirm("Continue (staging these notebooks first)?"): + not_in_project.append(pk_path) + if not_in_project: + not_in_project_string = "\n - ".join(not_in_project) + click.echo(f"Notebooks not in project:\n - {not_in_project_string}") + if not click.confirm("Continue (adding these files to the project)?"): click.secho("Aborted!", bold=True, fg="red") raise SystemExit(1) reader = click.prompt( @@ -55,8 +54,8 @@ def execute_nbs(cache_path, executor, pk_paths, timeout): default="nbformat", show_default=True, ) - for pk_path in unstaged: - record = db.stage_notebook_file(pk_path, reader=reader) + for pk_path in not_in_project: + record = db.add_nb_to_project(pk_path, reader=reader) records.append(record) try: executor = load_executor(executor, db, logger=logger) diff --git a/jupyter_cache/cli/commands/cmd_main.py b/jupyter_cache/cli/commands/cmd_main.py index a7388de..091e859 100644 --- a/jupyter_cache/cli/commands/cmd_main.py +++ b/jupyter_cache/cli/commands/cmd_main.py @@ -16,11 +16,15 @@ def jcache(*args, **kwargs): @jcache.command("clear") @options.CACHE_PATH -def clear_cache(cache_path): +@options.FORCE +def clear_cache(cache_path, force): """Clear the cache completely.""" from jupyter_cache.cache.main import JupyterCacheBase db = JupyterCacheBase(cache_path) - click.confirm("Are you sure you want to permanently clear the cache!?", abort=True) + if not force: + click.confirm( + "Are you sure you want to permanently clear the cache!?", abort=True + ) db.clear_cache() click.secho("Cache cleared!", fg="green") diff --git a/jupyter_cache/cli/commands/cmd_stage.py b/jupyter_cache/cli/commands/cmd_project.py similarity index 50% rename from jupyter_cache/cli/commands/cmd_stage.py rename to jupyter_cache/cli/commands/cmd_project.py index be70f41..9cc26ac 100644 --- a/jupyter_cache/cli/commands/cmd_stage.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -5,92 +5,91 @@ from jupyter_cache import get_cache from jupyter_cache.cli import arguments, options from jupyter_cache.cli.commands.cmd_main import jcache -from jupyter_cache.utils import tabulate_stage_records +from jupyter_cache.utils import tabulate_project_records -@jcache.group("stage") -def cmnd_stage(): - """Commands for staging notebooks to be executed.""" - pass +@jcache.group("project") +def cmnd_project(): + """Commands for interacting with a project.""" -@cmnd_stage.command("add") +@cmnd_project.command("add") @arguments.NB_PATHS @options.READER_KEY @options.CACHE_PATH -def stage_nbs(cache_path, nbpaths, reader): - """Stage notebook(s) for execution.""" +def add_notebooks(cache_path, nbpaths, reader): + """Add notebook(s) to the project.""" db = get_cache(cache_path) for path in nbpaths: # TODO deal with errors (print all at end? or option to ignore) - click.echo("Staging: {}".format(path)) - db.stage_notebook_file(path, reader=reader) + click.echo("Adding: {}".format(path)) + db.add_nb_to_project(path, reader=reader) click.secho("Success!", fg="green") -@cmnd_stage.command("add-with-assets") +@cmnd_project.command("add-with-assets") @arguments.ASSET_PATHS @options.NB_PATH @options.READER_KEY @options.CACHE_PATH -def stage_nb(cache_path, nbpath, reader, asset_paths): - """Stage a notebook, with possible asset files.""" +def add_notebook(cache_path, nbpath, reader, asset_paths): + """Add notebook(s) to the project, with possible asset files.""" db = get_cache(cache_path) - db.stage_notebook_file(nbpath, reader=reader, assets=asset_paths) + db.add_nb_to_project(nbpath, reader=reader, assets=asset_paths) click.secho("Success!", fg="green") -@cmnd_stage.command("remove-uris") +@cmnd_project.command("remove-uris") @arguments.NB_PATHS @options.CACHE_PATH @options.REMOVE_ALL -def unstage_nbs_uri(cache_path, nbpaths, remove_all): - """Un-stage notebook(s), by URI.""" +def remove_nbs_uri(cache_path, nbpaths, remove_all): + """Remove notebook(s) from the project, by URI.""" db = get_cache(cache_path) if remove_all: - nbpaths = [record.uri for record in db.list_staged_records()] + nbpaths = [record.uri for record in db.nb_project_records()] for path in nbpaths: # TODO deal with errors (print all at end? or option to ignore) - click.echo("Unstaging: {}".format(path)) - db.discard_staged_notebook(path) + click.echo("Removing: {}".format(path)) + db.remove_nb_from_project(path) click.secho("Success!", fg="green") -@cmnd_stage.command("remove-ids") +@cmnd_project.command("remove-ids") @arguments.PKS @options.CACHE_PATH @options.REMOVE_ALL -def unstage_nbs_id(cache_path, pks, remove_all): - """Un-stage notebook(s), by ID.""" +def remove_nbs_id(cache_path, pks, remove_all): + """Remove notebook(s) from the project, by ID.""" db = get_cache(cache_path) if remove_all: - pks = [record.pk for record in db.list_staged_records()] + pks = [record.pk for record in db.nb_project_records()] for pk in pks: # TODO deal with errors (print all at end? or option to ignore) - click.echo("Unstaging ID: {}".format(pk)) - db.discard_staged_notebook(pk) + click.echo("Removing: {}".format(pk)) + db.remove_nb_from_project(pk) click.secho("Success!", fg="green") -@cmnd_stage.command("list") +@cmnd_project.command("list") @options.CACHE_PATH -@click.option( - "--compare/--no-compare", - default=True, - show_default=True, - help="Compare to cached notebooks (to find cache ID).", -) +# @click.option( +# "--compare/--no-compare", +# default=True, +# show_default=True, +# help="Compare to cached notebooks (to find cache ID).", +# ) @options.PATH_LENGTH -def list_staged(cache_path, compare, path_length): - """List notebooks staged for possible execution.""" +def list_nbs_in_project(cache_path, path_length): + """List notebooks in the project.""" db = get_cache(cache_path) - records = db.list_staged_records() + records = db.nb_project_records() if not records: - click.secho("No Staged Notebooks", fg="blue") - click.echo(tabulate_stage_records(records, path_length=path_length, cache=db)) + click.secho("No notebooks in project", fg="blue") + click.echo(tabulate_project_records(records, path_length=path_length, cache=db)) -@cmnd_stage.command("show") +@cmnd_project.command("show") @options.CACHE_PATH @arguments.PK @click.option( @@ -99,17 +98,17 @@ def list_staged(cache_path, compare, path_length): show_default=True, help="Show traceback, if last execution failed.", ) -def show_staged(cache_path, pk, tb): - """Show details of a staged notebook.""" +def show_project_record(cache_path, pk, tb): + """Show details of a notebook.""" import yaml db = get_cache(cache_path) try: - record = db.get_staged_record(pk) + record = db.get_project_record(pk) except KeyError: click.secho("ID {} does not exist, Aborting!".format(pk), fg="red") sys.exit(1) - cache_record = db.get_cache_record_of_staged(record.uri) + cache_record = db.get_cached_project_nb(record.uri) data = record.format_dict(cache_record=cache_record, path_length=None, assets=False) click.echo(yaml.safe_dump(data, sort_keys=False).rstrip()) if record.assets: diff --git a/jupyter_cache/cli/options.py b/jupyter_cache/cli/options.py index 0ae51d0..48aa180 100644 --- a/jupyter_cache/cli/options.py +++ b/jupyter_cache/cli/options.py @@ -123,6 +123,10 @@ def check_cache_exists(ctx, param, value): help="Whether to overwrite an existing notebook with the same hash.", ) +FORCE = click.option( + "-f", "--force", default=False, is_flag=True, help="Do not ask for confirmation." +) + def confirm_remove_all(ctx, param, remove_all): if remove_all and not click.confirm("Are you sure you want to remove all?"): diff --git a/jupyter_cache/executors/base.py b/jupyter_cache/executors/base.py index e15ec5c..869a542 100644 --- a/jupyter_cache/executors/base.py +++ b/jupyter_cache/executors/base.py @@ -72,16 +72,10 @@ def run_and_cache( ) -> ExecutorRunResult: """Run execution, cache successfully executed notebooks and return their URIs - Parameters - ---------- - filter_uris: list - If specified filter the staged notebooks to execute by these URIs - filter_pks: list - If specified filter the staged notebooks to execute by these PKs - timeout: int - Maximum time in seconds to wait for a single cell to run for - allow_errors: bool - Whether to halt execution on the first cell exception + :param filter_uris: Filter the notebooks in the project to execute by these URIs + :param filter_pks: Filter the notebooks in the project to execute by these PKs + :param timeout: Maximum time in seconds to wait for a single cell to run for + :param allow_errors: Whether to halt execution on the first cell exception (provided the cell is not tagged as an expected exception) """ diff --git a/jupyter_cache/executors/basic.py b/jupyter_cache/executors/basic.py index 3d81258..32fbb76 100644 --- a/jupyter_cache/executors/basic.py +++ b/jupyter_cache/executors/basic.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Optional -from jupyter_cache.cache.db import NbStageRecord +from jupyter_cache.cache.db import NbProjectRecord from jupyter_cache.cache.main import NbArtifacts, NbBundleIn from jupyter_cache.executors.base import ExecutorRunResult, JupyterExecutorAbstract from jupyter_cache.executors.utils import single_nb_execution @@ -39,14 +39,16 @@ def run_and_cache( ) -> ExecutorRunResult: """This function interfaces with the cache, deferring execution to `execute`.""" # Get the notebook tha require re-execution - stage_records = self.cache.list_staged_unexecuted() + execute_records = self.cache.list_unexecuted() if filter_uris is not None: - stage_records = [r for r in stage_records if r.uri in filter_uris] + execute_records = [r for r in execute_records if r.uri in filter_uris] if filter_pks is not None: - stage_records = [r for r in stage_records if r.pk in filter_pks] + execute_records = [r for r in execute_records if r.pk in filter_pks] # remove any tracebacks from previous executions - NbStageRecord.remove_tracebacks([r.pk for r in stage_records], self.cache.db) + NbProjectRecord.remove_tracebacks( + [r.pk for r in execute_records], self.cache.db + ) # setup an dictionary to categorise all executed notebook uris: # excepted are where the actual notebook execution raised an exception; @@ -56,16 +58,17 @@ def run_and_cache( # so that we don't have to read all notebooks before execution def _iterator(): - for stage_record in stage_records: + for execute_record in execute_records: try: - nb_bundle = self.cache.get_staged_notebook(stage_record.pk) + nb_bundle = self.cache.get_project_notebook(execute_record.pk) except Exception: self.logger.error( - "Failed Retrieving: {}".format(stage_record.uri), exc_info=True + "Failed Retrieving: {}".format(execute_record.uri), + exc_info=True, ) - result.errored.append(stage_record.uri) + result.errored.append(execute_record.uri) else: - yield stage_record, nb_bundle + yield execute_record, nb_bundle # The execute method yields notebook bundles, or ExecutionError for bundle_or_exc in self.execute( @@ -81,7 +84,7 @@ def _iterator(): # The notebook raised an exception during execution # TODO store excepted bundles result.excepted.append(bundle_or_exc.uri) - NbStageRecord.set_traceback( + NbProjectRecord.set_traceback( bundle_or_exc.uri, bundle_or_exc.traceback, self.cache.db ) continue @@ -101,10 +104,10 @@ def _iterator(): # TODO it would also be ideal to tag all notebooks # that were executed at the same time (just part of `data` or separate column?). # TODO maybe the status of success/failure could be explicitly stored on - # the stage record (cache_status=Enum('OK', 'FAILED', 'MISSING')) + # the project record (cache_status=Enum('OK', 'FAILED', 'MISSING')) # although now traceback is so this is an implicit sign of failure, # TODO failed notebooks could be stored in the cache, which would be - # accessed by stage pk (and would be deleted when removing the stage record) + # accessed by the project pk (and would be deleted when removing the project record) # see: https://python.quantecon.org/status.html return result @@ -140,8 +143,8 @@ def execute_single( def execute(self, input_iterator, timeout=30, allow_errors=False): """This function is isolated from the cache, and is responsible for execution. - The method is only supplied with the staged record and input notebook bundle, - it then yield results for caching + The method is only supplied with the project record and input notebook bundle, + it then yields results for caching """ for _, nb_bundle in input_iterator: try: @@ -167,10 +170,10 @@ class JupyterExecutorTempSerial(JupyterExecutorLocalSerial): def execute(self, input_iterator, timeout=30, allow_errors=False): """This function is isolated from the cache, and is responsible for execution. - The method is only supplied with the staged record and input notebook bundle, - it then yield results for caching + The method is only supplied with the project record and input notebook bundle, + it then yields results for caching. """ - for stage_record, nb_bundle in input_iterator: + for execute_record, nb_bundle in input_iterator: try: uri = nb_bundle.uri self.logger.info("Executing: {}".format(uri)) @@ -178,7 +181,7 @@ def execute(self, input_iterator, timeout=30, allow_errors=False): with tempfile.TemporaryDirectory() as tmpdirname: try: - asset_files = _copy_assets(stage_record, tmpdirname) + asset_files = _copy_assets(execute_record, tmpdirname) except Exception as err: yield ExecutionError("Assets Retrieval Error", uri, err) continue diff --git a/jupyter_cache/utils.py b/jupyter_cache/utils.py index 9219c70..ffac542 100644 --- a/jupyter_cache/utils.py +++ b/jupyter_cache/utils.py @@ -91,10 +91,10 @@ def tabulate_cache_records(records: list, hashkeys=False, path_length=None) -> s ) -def tabulate_stage_records(records: list, path_length=None, cache=None) -> str: +def tabulate_project_records(records: list, path_length=None, cache=None) -> str: """Tabulate cache records. - :param records: list of ``NbStageRecord`` + :param records: list of ``NbProjectRecord`` :param path_length: truncate URI paths to x components :param cache: If the cache is given, we use it to add a column of matched cached pk (if available) @@ -105,7 +105,7 @@ def tabulate_stage_records(records: list, path_length=None, cache=None) -> str: for record in sorted(records, key=lambda r: r.created, reverse=True): cache_record = None if cache is not None: - cache_record = cache.get_cache_record_of_staged(record.uri) + cache_record = cache.get_cached_project_nb(record.uri) rows.append( record.format_dict(cache_record=cache_record, path_length=path_length) ) diff --git a/tests/make_cli_readme.py b/tests/make_cli_readme.py index 1f727f4..87a60c0 100644 --- a/tests/make_cli_readme.py +++ b/tests/make_cli_readme.py @@ -5,7 +5,7 @@ from click.testing import CliRunner from jupyter_cache.cache.main import DEFAULT_CACHE_LIMIT -from jupyter_cache.cli.commands import cmd_cache, cmd_exec, cmd_main, cmd_stage +from jupyter_cache.cli.commands import cmd_cache, cmd_exec, cmd_main, cmd_project def get_string(cli, group=None, args=(), input=None): @@ -94,7 +94,7 @@ def main(): for what has been cached. Each notebook is hashed (code cells and kernel spec only), - which is used to compare against 'staged' notebooks. + which is used to compare against notebooks in the project. Multiple hashes for the same URI can be added (the URI is just there for inspection) and the size of the cache is limited (current default {}) so that, at this size, @@ -163,25 +163,25 @@ def main(): get_string(cmd_cache.diff_nb, cache_name, ["2", "tests/notebooks/basic.ipynb"]) ) - # staging - strings.append("## Staging Notebooks for execution") - stage_name = cmd_stage.cmnd_stage.name - strings.append(get_string(cmd_stage.cmnd_stage, None, ["--help"])) + strings.append("## Adding notebooks to the project") + project_cmd_name = cmd_project.cmnd_project.name + strings.append(get_string(cmd_project.cmnd_project, None, ["--help"])) strings.append( dedent( """\ - Staged notebooks are recorded as pointers to their URI, + A project consist of a set of notebooks to be executed. + + Notebooks are recorded as pointers to their URI (e.g. file path), i.e. no physical copying takes place until execution time. - If you stage some notebooks for execution, then - you can list them to see which have existing records in the cache (by hash), + You can list the notebooks to see which have existing records in the cache (by hash), and which will require execution:""" ) ) strings.append( get_string( - cmd_stage.stage_nbs, - stage_name, + cmd_project.add_notebooks, + project_cmd_name, [ "tests/notebooks/basic.ipynb", "tests/notebooks/basic_failing.ipynb", @@ -191,9 +191,9 @@ def main(): ], ) ) - strings.append(get_string(cmd_stage.list_staged, stage_name)) - strings.append("You can remove a staged notebook by its URI or ID:") - strings.append(get_string(cmd_stage.unstage_nbs_id, stage_name, ["4"])) + strings.append(get_string(cmd_project.list_nbs_in_project, project_cmd_name)) + strings.append("You can remove a notebook from the project by its URI or ID:") + strings.append(get_string(cmd_project.remove_nbs_id, project_cmd_name, ["4"])) strings.append("You can then run a basic execution of the required notebooks:") strings.append(get_string(cmd_cache.remove_caches, cache_name, ["6", "2"])) strings.append(get_string(cmd_exec.execute_nbs, None)) @@ -205,16 +205,16 @@ def main(): that are inside the notebook folder, and data supplied by the executor.""" ) ) - strings.append(get_string(cmd_stage.list_staged, stage_name)) + strings.append(get_string(cmd_project.list_nbs_in_project, project_cmd_name)) strings.append( "Execution data (such as execution time) will be stored in the cache record:" ) strings.append(get_string(cmd_cache.show_cache, cache_name, ["6"])) strings.append( "Failed notebooks will not be cached, " - "but the exception traceback will be added to the stage record:" + "but the exception traceback will be added to the notebook's project record:" ) - strings.append(get_string(cmd_stage.show_staged, stage_name, ["2"])) + strings.append(get_string(cmd_project.show_project_record, project_cmd_name, ["2"])) strings.append( dedent( """\ @@ -225,18 +225,18 @@ def main(): ) ) strings.append( - "Once executed you may leave staged notebooks, " + "Once executed you may leave notebooks in the project, " "for later re-execution, or remove them:" ) strings.append( - get_string(cmd_stage.unstage_nbs_id, stage_name, ["--all"], input="y") + get_string(cmd_project.remove_nbs_id, project_cmd_name, ["--all"], input="y") ) # assets strings.append( dedent( """\ - You can also stage notebooks with assets; + You can also add notebooks to the projects with assets; external files that are required by the notebook during execution. As with artefacts, these files must be in the same folder as the notebook, or a sub-folder.""" @@ -244,8 +244,8 @@ def main(): ) strings.append( get_string( - cmd_stage.stage_nb, - stage_name, + cmd_project.add_notebook, + project_cmd_name, [ "-nb", "tests/notebooks/basic.ipynb", @@ -253,7 +253,7 @@ def main(): ], ) ) - strings.append(get_string(cmd_stage.show_staged, stage_name, ["1"])) + strings.append(get_string(cmd_project.show_project_record, project_cmd_name, ["1"])) return "\n\n".join(strings) diff --git a/tests/test_cache.py b/tests/test_cache.py index edaa2f7..27ffc74 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -75,19 +75,19 @@ def test_basic_workflow(tmp_path): check_validity=False, ) with pytest.raises(ValueError): - cache.stage_notebook_file(os.path.join(NB_PATH, "basic.ipynb"), assets=[""]) - cache.stage_notebook_file( + cache.add_nb_to_project(os.path.join(NB_PATH, "basic.ipynb"), assets=[""]) + cache.add_nb_to_project( os.path.join(NB_PATH, "basic.ipynb"), assets=[os.path.join(NB_PATH, "basic.ipynb")], ) - assert [r.pk for r in cache.list_staged_records()] == [1] - assert [r.pk for r in cache.list_staged_unexecuted()] == [] + assert [r.pk for r in cache.nb_project_records()] == [1] + assert [r.pk for r in cache.list_unexecuted()] == [] - cache.stage_notebook_file(os.path.join(NB_PATH, "basic_failing.ipynb")) - assert [r.pk for r in cache.list_staged_records()] == [1, 2] - assert [r.pk for r in cache.list_staged_unexecuted()] == [2] + cache.add_nb_to_project(os.path.join(NB_PATH, "basic_failing.ipynb")) + assert [r.pk for r in cache.nb_project_records()] == [1, 2] + assert [r.pk for r in cache.list_unexecuted()] == [2] - bundle = cache.get_staged_notebook(os.path.join(NB_PATH, "basic_failing.ipynb")) + bundle = cache.get_project_notebook(os.path.join(NB_PATH, "basic_failing.ipynb")) assert bundle.nb.metadata cache.clear_cache() @@ -189,9 +189,9 @@ def test_execution(tmp_path, executor_key): db = JupyterCacheBase(str(tmp_path / "cache")) temp_nb_path = tmp_path / "notebooks" shutil.copytree(NB_PATH, temp_nb_path) - db.stage_notebook_file(path=os.path.join(temp_nb_path, "basic_unrun.ipynb")) - db.stage_notebook_file(path=os.path.join(temp_nb_path, "basic_failing.ipynb")) - db.stage_notebook_file( + db.add_nb_to_project(path=os.path.join(temp_nb_path, "basic_unrun.ipynb")) + db.add_nb_to_project(path=os.path.join(temp_nb_path, "basic_failing.ipynb")) + db.add_nb_to_project( path=os.path.join(temp_nb_path, "external_output.ipynb"), assets=(os.path.join(temp_nb_path, "basic.ipynb"),), ) @@ -221,9 +221,9 @@ def test_execution(tmp_path, executor_key): paths = [str(p.relative_to(path)) for p in path.glob("**/*") if p.is_file()] assert paths == ["artifact.txt"] assert path.joinpath("artifact.txt").read_text(encoding="utf8") == "hi" - stage_record = db.get_staged_record(2) - assert stage_record.traceback is not None - assert "Exception: oopsie!" in stage_record.traceback + project_record = db.get_project_record(2) + assert project_record.traceback is not None + assert "Exception: oopsie!" in project_record.traceback def test_execution_jupytext(tmp_path): @@ -233,9 +233,7 @@ def test_execution_jupytext(tmp_path): db = JupyterCacheBase(str(tmp_path / "cache")) temp_nb_path = tmp_path / "notebooks" shutil.copytree(NB_PATH, temp_nb_path) - db.stage_notebook_file( - path=os.path.join(temp_nb_path, "basic.md"), reader="jupytext" - ) + db.add_nb_to_project(path=os.path.join(temp_nb_path, "basic.md"), reader="jupytext") executor = load_executor("local-serial", db) result = executor.run_and_cache() print(result) @@ -254,7 +252,7 @@ def test_execution_timeout_config(tmp_path): from jupyter_cache.executors import load_executor db = JupyterCacheBase(str(tmp_path)) - db.stage_notebook_file(path=os.path.join(NB_PATH, "sleep_2.ipynb")) + db.add_nb_to_project(path=os.path.join(NB_PATH, "sleep_2.ipynb")) executor = load_executor("local-serial", db) result = executor.run_and_cache(timeout=10) assert result.as_json() == { @@ -264,7 +262,7 @@ def test_execution_timeout_config(tmp_path): } db.clear_cache() - db.stage_notebook_file(path=os.path.join(NB_PATH, "sleep_2.ipynb")) + db.add_nb_to_project(path=os.path.join(NB_PATH, "sleep_2.ipynb")) executor = load_executor("local-serial", db) result = executor.run_and_cache(timeout=1) assert result.as_json() == { @@ -279,7 +277,7 @@ def test_execution_timeout_metadata(tmp_path): from jupyter_cache.executors import load_executor db = JupyterCacheBase(str(tmp_path)) - db.stage_notebook_file(path=os.path.join(NB_PATH, "sleep_2_timeout_1.ipynb")) + db.add_nb_to_project(path=os.path.join(NB_PATH, "sleep_2_timeout_1.ipynb")) executor = load_executor("local-serial", db) result = executor.run_and_cache() assert result.as_json() == { @@ -294,7 +292,7 @@ def test_execution_allow_errors_config(tmp_path): from jupyter_cache.executors import load_executor db = JupyterCacheBase(str(tmp_path)) - db.stage_notebook_file(path=os.path.join(NB_PATH, "basic_failing.ipynb")) + db.add_nb_to_project(path=os.path.join(NB_PATH, "basic_failing.ipynb")) executor = load_executor("local-serial", db) result = executor.run_and_cache(allow_errors=True) assert result.as_json() == { @@ -309,7 +307,7 @@ def test_run_in_temp_false(tmp_path): from jupyter_cache.executors import load_executor db = JupyterCacheBase(str(tmp_path)) - db.stage_notebook_file(path=os.path.join(NB_PATH, "basic.ipynb")) + db.add_nb_to_project(path=os.path.join(NB_PATH, "basic.ipynb")) executor = load_executor("temp-serial", db) result = executor.run_and_cache() assert result.as_json() == { diff --git a/tests/test_cli.py b/tests/test_cli.py index b9a6359..f58c115 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,7 +3,7 @@ from click.testing import CliRunner from jupyter_cache.cache.main import JupyterCacheBase -from jupyter_cache.cli.commands import cmd_cache, cmd_main, cmd_stage +from jupyter_cache.cli.commands import cmd_cache, cmd_main, cmd_project NB_PATH = os.path.join(os.path.realpath(os.path.dirname(__file__)), "notebooks") @@ -151,53 +151,53 @@ def test_diff_nbs(tmp_path): ] -def test_stage_nbs(tmp_path): +def test_add_nbs_to_project(tmp_path): db = JupyterCacheBase(str(tmp_path)) path = os.path.join(NB_PATH, "basic.ipynb") runner = CliRunner() - result = runner.invoke(cmd_stage.stage_nbs, ["-p", tmp_path, path]) + result = runner.invoke(cmd_project.add_notebooks, ["-p", tmp_path, path]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output - assert db.list_staged_records()[0].uri == path + assert db.nb_project_records()[0].uri == path -def test_unstage_nbs(tmp_path): +def test_remove_nbs_from_project(tmp_path): db = JupyterCacheBase(str(tmp_path)) path = os.path.join(NB_PATH, "basic.ipynb") runner = CliRunner() - result = runner.invoke(cmd_stage.stage_nbs, ["-p", tmp_path, path]) - result = runner.invoke(cmd_stage.unstage_nbs_uri, ["-p", tmp_path, path]) + result = runner.invoke(cmd_project.add_notebooks, ["-p", tmp_path, path]) + result = runner.invoke(cmd_project.remove_nbs_uri, ["-p", tmp_path, path]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output - assert db.list_staged_records() == [] + assert db.nb_project_records() == [] -def test_list_staged(tmp_path): +def test_list_nbs_in_project(tmp_path): db = JupyterCacheBase(str(tmp_path)) db.cache_notebook_file( path=os.path.join(NB_PATH, "basic.ipynb"), check_validity=False ) - db.stage_notebook_file(path=os.path.join(NB_PATH, "basic.ipynb")) - db.stage_notebook_file(path=os.path.join(NB_PATH, "basic_failing.ipynb")) + db.add_nb_to_project(path=os.path.join(NB_PATH, "basic.ipynb")) + db.add_nb_to_project(path=os.path.join(NB_PATH, "basic_failing.ipynb")) runner = CliRunner() - result = runner.invoke(cmd_stage.list_staged, ["-p", tmp_path]) + result = runner.invoke(cmd_project.list_nbs_in_project, ["-p", tmp_path]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output -def test_show_staged(tmp_path): +def test_show_project_record(tmp_path): db = JupyterCacheBase(str(tmp_path)) db.cache_notebook_file( path=os.path.join(NB_PATH, "basic.ipynb"), check_validity=False ) - db.stage_notebook_file(path=os.path.join(NB_PATH, "basic.ipynb")) + db.add_nb_to_project(path=os.path.join(NB_PATH, "basic.ipynb")) runner = CliRunner() - result = runner.invoke(cmd_stage.show_staged, ["-p", tmp_path, "1"]) + result = runner.invoke(cmd_project.show_project_record, ["-p", tmp_path, "1"]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output From dfba7b0690d31a5f5000f2aaf371ce9fc79f9689 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 2 Aug 2021 05:22:09 +0200 Subject: [PATCH 05/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20REFACTOR:=20Consolid?= =?UTF-8?q?ate=20`remove-ids`/`remove-uris`=20->=20`remove`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For `jcache project` commands, and remove `--all` option, in favour of separate `jcache project clear` --- docs/using/api.ipynb | 12 ++--- docs/using/cli.md | 66 +++++++++++------------ jupyter_cache/base.py | 2 +- jupyter_cache/cache/main.py | 4 +- jupyter_cache/cli/commands/cmd_cache.py | 7 +-- jupyter_cache/cli/commands/cmd_project.py | 42 +++++++-------- tests/make_cli_readme.py | 8 ++- tests/test_cache.py | 4 +- tests/test_cli.py | 6 +-- 9 files changed, 71 insertions(+), 80 deletions(-) diff --git a/docs/using/api.ipynb b/docs/using/api.ipynb index 215da4f..405afdf 100644 --- a/docs/using/api.ipynb +++ b/docs/using/api.ipynb @@ -96,7 +96,7 @@ "execution_count": 3, "source": [ "print(cache.list_cache_records())\n", - "print(cache.nb_project_records())" + "print(cache.list_project_records())" ], "outputs": [ { @@ -734,7 +734,7 @@ "execution_count": 25, "source": [ "print(tabulate_project_records(\n", - " cache.nb_project_records(), path_length=2, cache=cache\n", + " cache.list_project_records(), path_length=2, cache=cache\n", "))" ], "outputs": [ @@ -871,7 +871,7 @@ "execution_count": 30, "source": [ "print(tabulate_project_records(\n", - " cache.nb_project_records(), path_length=2, cache=cache\n", + " cache.list_project_records(), path_length=2, cache=cache\n", "))" ], "outputs": [ @@ -909,7 +909,7 @@ "execution_count": 32, "source": [ "print(tabulate_project_records(\n", - " cache.nb_project_records(), path_length=2, cache=cache\n", + " cache.list_project_records(), path_length=2, cache=cache\n", "))" ], "outputs": [ @@ -968,7 +968,7 @@ "execution_count": 34, "source": [ "print(tabulate_project_records(\n", - " cache.nb_project_records(), path_length=2, cache=cache\n", + " cache.list_project_records(), path_length=2, cache=cache\n", "))" ], "outputs": [ @@ -1214,7 +1214,7 @@ "execution_count": 41, "source": [ "print(tabulate_project_records(\n", - " cache.nb_project_records(), path_length=2, cache=cache\n", + " cache.list_project_records(), path_length=2, cache=cache\n", "))" ], "outputs": [ diff --git a/docs/using/cli.md b/docs/using/cli.md index 6624fc9..e06254f 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -2,7 +2,7 @@ # Command-Line - + From the checked-out repository folder: @@ -105,10 +105,10 @@ You can remove cached records by their ID. $ jcache cache list ID Origin URI Created Accessed ---- ------------------------------------- ---------------- ---------------- - 5 tests/notebooks/external_output.ipynb 2021-08-02 01:51 2021-08-02 01:51 - 4 tests/notebooks/complex_outputs.ipynb 2021-08-02 01:51 2021-08-02 01:51 - 3 tests/notebooks/basic_unrun.ipynb 2021-08-02 01:51 2021-08-02 01:51 - 2 tests/notebooks/basic_failing.ipynb 2021-08-02 01:51 2021-08-02 01:51 + 5 tests/notebooks/external_output.ipynb 2021-08-02 03:12 2021-08-02 03:12 + 4 tests/notebooks/complex_outputs.ipynb 2021-08-02 03:12 2021-08-02 03:12 + 3 tests/notebooks/basic_unrun.ipynb 2021-08-02 03:12 2021-08-02 03:12 + 2 tests/notebooks/basic_failing.ipynb 2021-08-02 03:12 2021-08-02 03:12 ``` ````{tip} @@ -136,8 +136,8 @@ Show a full description of a cached notebook by referring to its ID $ jcache cache show 6 ID: 6 Origin URI: ../tests/notebooks/basic.ipynb -Created: 2021-08-02 01:51 -Accessed: 2021-08-02 01:51 +Created: 2021-08-02 03:12 +Accessed: 2021-08-02 03:12 Hashkey: 94c17138f782c75df59e989fffa64e3a Artifacts: - artifact_folder/artifact.txt @@ -202,7 +202,7 @@ Success! $ jcache project --help Usage: project [OPTIONS] COMMAND [ARGS]... - Commands for creating/interacting with a project of notebooks. + Commands for interacting with a project. Options: --help Show this message and exit. @@ -210,9 +210,9 @@ Options: Commands: add Add notebook(s) to the project. add-with-assets Add notebook(s) to the project, with possible asset files. + clear Remove all notebooks from the project. list List notebooks in the project. - remove-ids Remove notebook(s) from the project, by ID. - remove-uris Remove notebook(s) from the project, by URI. + remove Remove notebook(s) from the project, by ID/URI. show Show details of a notebook. ``` @@ -238,17 +238,17 @@ Success! $ jcache project list ID URI Reader Created Assets Cache ID ---- ------------------------------------- -------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 01:51 0 5 - 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-02 01:51 0 - 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 01:51 0 6 - 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 01:51 0 2 - 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 01:51 0 6 + 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 03:12 0 5 + 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-02 03:12 0 + 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 03:12 0 6 + 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 03:12 0 2 + 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 03:12 0 6 ``` You can remove a notebook from the project by its URI or ID: ```console -$ jcache project remove-ids 4 +$ jcache project remove 4 Removing: 4 Success! ``` @@ -270,12 +270,15 @@ Executing: ../tests/notebooks/basic_failing.ipynb error: Execution Failed: ../tests/notebooks/basic_failing.ipynb Executing: ../tests/notebooks/basic_unrun.ipynb Execution Succeeded: ../tests/notebooks/basic_unrun.ipynb +Executing: ../tests/notebooks/complex_outputs.ipynb +error: Execution Failed: ../tests/notebooks/complex_outputs.ipynb Finished! Successfully executed notebooks have been cached. succeeded: - ../tests/notebooks/basic.ipynb - ../tests/notebooks/basic_unrun.ipynb excepted: - ../tests/notebooks/basic_failing.ipynb +- ../tests/notebooks/complex_outputs.ipynb errored: [] up-to-date: [] @@ -289,10 +292,11 @@ that are inside the notebook folder, and data supplied by the executor. $ jcache project list ID URI Reader Created Assets Cache ID ---- ------------------------------------- -------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 01:51 0 5 - 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 01:51 0 6 - 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 01:51 0 - 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 01:51 0 6 + 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 03:12 0 5 + 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-02 03:12 0 + 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 03:12 0 6 + 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 03:12 0 + 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 03:12 0 6 ``` Execution data (such as execution time) will be stored in the cache record: @@ -301,11 +305,11 @@ Execution data (such as execution time) will be stored in the cache record: $ jcache cache show 6 ID: 6 Origin URI: ../tests/notebooks/basic_unrun.ipynb -Created: 2021-08-02 01:51 -Accessed: 2021-08-02 01:51 +Created: 2021-08-02 03:12 +Accessed: 2021-08-02 03:12 Hashkey: 94c17138f782c75df59e989fffa64e3a Data: - execution_seconds: 2.027238027 + execution_seconds: 1.639128618 ``` @@ -316,7 +320,7 @@ $ jcache project show 2 ID: 2 URI: ../tests/notebooks/basic_failing.ipynb Reader: nbformat -Created: 2021-08-02 01:51 +Created: 2021-08-02 03:12 Failed Last Execution! Traceback (most recent call last): File "../jupyter_cache/executors/utils.py", line 55, in single_nb_execution @@ -342,7 +346,7 @@ raise Exception('oopsie!') --------------------------------------------------------------------------- Exception Traceback (most recent call last) -/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_76025/340246212.py in +/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_86857/340246212.py in ----> 1 raise Exception('oopsie!') Exception: oopsie! @@ -359,13 +363,9 @@ Code cells can be tagged with `raises-exception` to let the executor known that Once executed you may leave notebooks in the project, for later re-execution, or remove them: ```console -$ jcache project remove-ids --all -Are you sure you want to remove all? [y/N]: y -Removing: 1 -Removing: 2 -Removing: 3 -Removing: 5 -Success! +$ jcache project clear +Are you sure you want to permanently clear the project!? [y/N]: y +Project cleared! ``` You can also add notebooks to the projects with assets; @@ -383,7 +383,7 @@ $ jcache project show 1 ID: 1 URI: ../tests/notebooks/basic.ipynb Reader: nbformat -Created: 2021-08-02 01:51 +Created: 2021-08-02 03:12 Cache ID: 6 Assets: - ../tests/notebooks/artifact_folder/artifact.txt diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index 3eebfce..670295f 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -258,7 +258,7 @@ def remove_nb_from_project(self, uri_or_pk: Union[int, str]): """Remove a notebook from the project.""" @abstractmethod - def nb_project_records(self) -> List[NbProjectRecord]: + def list_project_records(self) -> List[NbProjectRecord]: """Return a list of the notebook URI's in the project.""" @abstractmethod diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 23b09e1..7528550 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -415,7 +415,7 @@ def add_nb_to_project( # TODO physically copy to cache? # TODO assets - def nb_project_records(self) -> List[NbProjectRecord]: + def list_project_records(self) -> List[NbProjectRecord]: return NbProjectRecord.records_all(self.db) def get_project_record(self, uri_or_pk: Union[int, str]) -> NbProjectRecord: @@ -462,7 +462,7 @@ def get_cached_project_nb( def list_unexecuted(self) -> List[NbProjectRecord]: records = [] - for record in self.nb_project_records(): + for record in self.list_project_records(): nb = self.get_project_notebook(record.uri).nb _, hashkey = self.create_hashed_notebook(nb) try: diff --git a/jupyter_cache/cli/commands/cmd_cache.py b/jupyter_cache/cli/commands/cmd_cache.py index cd88162..3373d5a 100644 --- a/jupyter_cache/cli/commands/cmd_cache.py +++ b/jupyter_cache/cli/commands/cmd_cache.py @@ -165,11 +165,8 @@ def clear_cache_cmd(cache_path, force): click.confirm( "Are you sure you want to permanently clear the cache!?", abort=True ) - for pk in db.list_cache_records(): - try: - db.remove_cache(pk) - except Exception: - pass + for record in db.list_cache_records(): + db.remove_cache(record.pk) click.secho("Cache cleared!", fg="green") diff --git a/jupyter_cache/cli/commands/cmd_project.py b/jupyter_cache/cli/commands/cmd_project.py index 9cc26ac..2821f0e 100644 --- a/jupyter_cache/cli/commands/cmd_project.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -39,35 +39,31 @@ def add_notebook(cache_path, nbpath, reader, asset_paths): click.secho("Success!", fg="green") -@cmnd_project.command("remove-uris") -@arguments.NB_PATHS +@cmnd_project.command("clear") @options.CACHE_PATH -@options.REMOVE_ALL -def remove_nbs_uri(cache_path, nbpaths, remove_all): - """Remove notebook(s) from the project, by URI.""" +@options.FORCE +def clear_nbs(cache_path, force): + """Remove all notebooks from the project.""" db = get_cache(cache_path) - if remove_all: - nbpaths = [record.uri for record in db.nb_project_records()] - for path in nbpaths: - # TODO deal with errors (print all at end? or option to ignore) - click.echo("Removing: {}".format(path)) - db.remove_nb_from_project(path) - click.secho("Success!", fg="green") + if not force: + click.confirm( + "Are you sure you want to permanently clear the project!?", abort=True + ) + for record in db.list_project_records(): + db.remove_nb_from_project(record.pk) + click.secho("Project cleared!", fg="green") -@cmnd_project.command("remove-ids") -@arguments.PKS +@cmnd_project.command("remove") +@arguments.PK_OR_PATHS @options.CACHE_PATH -@options.REMOVE_ALL -def remove_nbs_id(cache_path, pks, remove_all): - """Remove notebook(s) from the project, by ID.""" +def remove_nbs(cache_path, pk_paths): + """Remove notebook(s) from the project, by ID/URI.""" db = get_cache(cache_path) - if remove_all: - pks = [record.pk for record in db.nb_project_records()] - for pk in pks: + for path in pk_paths: # TODO deal with errors (print all at end? or option to ignore) - click.echo("Removing: {}".format(pk)) - db.remove_nb_from_project(pk) + click.echo("Removing: {}".format(path)) + db.remove_nb_from_project(path) click.secho("Success!", fg="green") @@ -83,7 +79,7 @@ def remove_nbs_id(cache_path, pks, remove_all): def list_nbs_in_project(cache_path, path_length): """List notebooks in the project.""" db = get_cache(cache_path) - records = db.nb_project_records() + records = db.list_project_records() if not records: click.secho("No notebooks in project", fg="blue") click.echo(tabulate_project_records(records, path_length=path_length, cache=db)) diff --git a/tests/make_cli_readme.py b/tests/make_cli_readme.py index 87a60c0..cc9c0fa 100644 --- a/tests/make_cli_readme.py +++ b/tests/make_cli_readme.py @@ -20,7 +20,7 @@ def get_string(cli, group=None, args=(), input=None): root_path = os.getcwd() + os.sep output = result.output.replace(root_path, "../") if result.exception: - output += "\n" + str(result.exception) + output += "\n" + str(result.exception) + "\n" return "```console\n$ {}{}\n{}```".format( command_str, (" " + " ".join(args)) if args else "", @@ -193,7 +193,7 @@ def main(): ) strings.append(get_string(cmd_project.list_nbs_in_project, project_cmd_name)) strings.append("You can remove a notebook from the project by its URI or ID:") - strings.append(get_string(cmd_project.remove_nbs_id, project_cmd_name, ["4"])) + strings.append(get_string(cmd_project.remove_nbs, project_cmd_name, ["4"])) strings.append("You can then run a basic execution of the required notebooks:") strings.append(get_string(cmd_cache.remove_caches, cache_name, ["6", "2"])) strings.append(get_string(cmd_exec.execute_nbs, None)) @@ -228,9 +228,7 @@ def main(): "Once executed you may leave notebooks in the project, " "for later re-execution, or remove them:" ) - strings.append( - get_string(cmd_project.remove_nbs_id, project_cmd_name, ["--all"], input="y") - ) + strings.append(get_string(cmd_project.clear_nbs, project_cmd_name, [], input="y")) # assets strings.append( diff --git a/tests/test_cache.py b/tests/test_cache.py index 27ffc74..12db3f4 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -80,11 +80,11 @@ def test_basic_workflow(tmp_path): os.path.join(NB_PATH, "basic.ipynb"), assets=[os.path.join(NB_PATH, "basic.ipynb")], ) - assert [r.pk for r in cache.nb_project_records()] == [1] + assert [r.pk for r in cache.list_project_records()] == [1] assert [r.pk for r in cache.list_unexecuted()] == [] cache.add_nb_to_project(os.path.join(NB_PATH, "basic_failing.ipynb")) - assert [r.pk for r in cache.nb_project_records()] == [1, 2] + assert [r.pk for r in cache.list_project_records()] == [1, 2] assert [r.pk for r in cache.list_unexecuted()] == [2] bundle = cache.get_project_notebook(os.path.join(NB_PATH, "basic_failing.ipynb")) diff --git a/tests/test_cli.py b/tests/test_cli.py index f58c115..454d4dc 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -159,7 +159,7 @@ def test_add_nbs_to_project(tmp_path): assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output - assert db.nb_project_records()[0].uri == path + assert db.list_project_records()[0].uri == path def test_remove_nbs_from_project(tmp_path): @@ -167,11 +167,11 @@ def test_remove_nbs_from_project(tmp_path): path = os.path.join(NB_PATH, "basic.ipynb") runner = CliRunner() result = runner.invoke(cmd_project.add_notebooks, ["-p", tmp_path, path]) - result = runner.invoke(cmd_project.remove_nbs_uri, ["-p", tmp_path, path]) + result = runner.invoke(cmd_project.remove_nbs, ["-p", tmp_path, path]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output - assert db.nb_project_records() == [] + assert db.list_project_records() == [] def test_list_nbs_in_project(tmp_path): From fe4eaa65c697f01e0f8ef7f2c1b3d65383ac63c3 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 2 Aug 2021 08:10:16 +0200 Subject: [PATCH 06/39] =?UTF-8?q?=E2=9C=A8=20NEW:=20Add=20`jbcache=20proje?= =?UTF-8?q?ct=20merge`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To write out a notebook merging the project file with its cached outputs. --- docs/using/cli.md | 57 +++++++++++------------ jupyter_cache/cache/main.py | 6 +-- jupyter_cache/cli/arguments.py | 8 ++++ jupyter_cache/cli/commands/cmd_cache.py | 2 +- jupyter_cache/cli/commands/cmd_project.py | 25 ++++++++-- tests/make_cli_readme.py | 9 +++- tests/test_cli.py | 29 ++++++++++++ tox.ini | 2 +- 8 files changed, 94 insertions(+), 44 deletions(-) diff --git a/docs/using/cli.md b/docs/using/cli.md index e06254f..ffccde0 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -2,7 +2,7 @@ # Command-Line - + From the checked-out repository folder: @@ -19,7 +19,7 @@ Options: -h, --help Show this message and exit. Commands: - cache Commands for adding to and inspecting the cache. + cache Commands for interacting with cached executions. clear Clear the cache completely. config Commands for configuring the cache. execute Execute all or specific outdated notebooks in the project. @@ -40,7 +40,7 @@ eval "$(_JCACHE_COMPLETE=source jcache)" $ jcache cache --help Usage: cache [OPTIONS] COMMAND [ARGS]... - Commands for adding to and inspecting the cache. + Commands for interacting with cached executions. Options: --help Show this message and exit. @@ -105,10 +105,10 @@ You can remove cached records by their ID. $ jcache cache list ID Origin URI Created Accessed ---- ------------------------------------- ---------------- ---------------- - 5 tests/notebooks/external_output.ipynb 2021-08-02 03:12 2021-08-02 03:12 - 4 tests/notebooks/complex_outputs.ipynb 2021-08-02 03:12 2021-08-02 03:12 - 3 tests/notebooks/basic_unrun.ipynb 2021-08-02 03:12 2021-08-02 03:12 - 2 tests/notebooks/basic_failing.ipynb 2021-08-02 03:12 2021-08-02 03:12 + 5 tests/notebooks/external_output.ipynb 2021-08-02 06:06 2021-08-02 06:06 + 4 tests/notebooks/complex_outputs.ipynb 2021-08-02 06:06 2021-08-02 06:06 + 3 tests/notebooks/basic_unrun.ipynb 2021-08-02 06:06 2021-08-02 06:06 + 2 tests/notebooks/basic_failing.ipynb 2021-08-02 06:06 2021-08-02 06:06 ``` ````{tip} @@ -136,8 +136,8 @@ Show a full description of a cached notebook by referring to its ID $ jcache cache show 6 ID: 6 Origin URI: ../tests/notebooks/basic.ipynb -Created: 2021-08-02 03:12 -Accessed: 2021-08-02 03:12 +Created: 2021-08-02 06:06 +Accessed: 2021-08-02 06:06 Hashkey: 94c17138f782c75df59e989fffa64e3a Artifacts: - artifact_folder/artifact.txt @@ -212,8 +212,9 @@ Commands: add-with-assets Add notebook(s) to the project, with possible asset files. clear Remove all notebooks from the project. list List notebooks in the project. - remove Remove notebook(s) from the project, by ID/URI. - show Show details of a notebook. + merge Write notebook merged with cached outputs (by ID/URI). + remove Remove notebook(s) from the project (by ID/URI). + show Show details of a notebook (by ID). ``` A project consist of a set of notebooks to be executed. @@ -238,11 +239,11 @@ Success! $ jcache project list ID URI Reader Created Assets Cache ID ---- ------------------------------------- -------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 03:12 0 5 - 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-02 03:12 0 - 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 03:12 0 6 - 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 03:12 0 2 - 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 03:12 0 6 + 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 06:06 0 5 + 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-02 06:06 0 + 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 06:06 0 6 + 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 06:06 0 2 + 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 06:06 0 6 ``` You can remove a notebook from the project by its URI or ID: @@ -270,15 +271,12 @@ Executing: ../tests/notebooks/basic_failing.ipynb error: Execution Failed: ../tests/notebooks/basic_failing.ipynb Executing: ../tests/notebooks/basic_unrun.ipynb Execution Succeeded: ../tests/notebooks/basic_unrun.ipynb -Executing: ../tests/notebooks/complex_outputs.ipynb -error: Execution Failed: ../tests/notebooks/complex_outputs.ipynb Finished! Successfully executed notebooks have been cached. succeeded: - ../tests/notebooks/basic.ipynb - ../tests/notebooks/basic_unrun.ipynb excepted: - ../tests/notebooks/basic_failing.ipynb -- ../tests/notebooks/complex_outputs.ipynb errored: [] up-to-date: [] @@ -292,11 +290,10 @@ that are inside the notebook folder, and data supplied by the executor. $ jcache project list ID URI Reader Created Assets Cache ID ---- ------------------------------------- -------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 03:12 0 5 - 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-02 03:12 0 - 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 03:12 0 6 - 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 03:12 0 - 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 03:12 0 6 + 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 06:06 0 5 + 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 06:06 0 6 + 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 06:06 0 + 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 06:06 0 6 ``` Execution data (such as execution time) will be stored in the cache record: @@ -305,11 +302,11 @@ Execution data (such as execution time) will be stored in the cache record: $ jcache cache show 6 ID: 6 Origin URI: ../tests/notebooks/basic_unrun.ipynb -Created: 2021-08-02 03:12 -Accessed: 2021-08-02 03:12 +Created: 2021-08-02 06:06 +Accessed: 2021-08-02 06:06 Hashkey: 94c17138f782c75df59e989fffa64e3a Data: - execution_seconds: 1.639128618 + execution_seconds: 0.887201879 ``` @@ -320,7 +317,7 @@ $ jcache project show 2 ID: 2 URI: ../tests/notebooks/basic_failing.ipynb Reader: nbformat -Created: 2021-08-02 03:12 +Created: 2021-08-02 06:06 Failed Last Execution! Traceback (most recent call last): File "../jupyter_cache/executors/utils.py", line 55, in single_nb_execution @@ -346,7 +343,7 @@ raise Exception('oopsie!') --------------------------------------------------------------------------- Exception Traceback (most recent call last) -/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_86857/340246212.py in +/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_3944/340246212.py in ----> 1 raise Exception('oopsie!') Exception: oopsie! @@ -383,7 +380,7 @@ $ jcache project show 1 ID: 1 URI: ../tests/notebooks/basic.ipynb Reader: nbformat -Created: 2021-08-02 03:12 +Created: 2021-08-02 06:06 Cache ID: 6 Assets: - ../tests/notebooks/artifact_folder/artifact.txt diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 7528550..d4845ec 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -449,11 +449,7 @@ def get_project_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: def get_cached_project_nb( self, uri_or_pk: Union[int, str] ) -> Optional[NbCacheRecord]: - if isinstance(uri_or_pk, int): - record = NbProjectRecord.record_from_pk(uri_or_pk, self.db) - else: - record = NbProjectRecord.record_from_uri(uri_or_pk, self.db) - nb = self.get_project_notebook(record.uri).nb + nb = self.get_project_notebook(uri_or_pk).nb _, hashkey = self.create_hashed_notebook(nb) try: return NbCacheRecord.record_from_hashkey(hashkey, self.db) diff --git a/jupyter_cache/cli/arguments.py b/jupyter_cache/cli/arguments.py index 6aa3d48..7f2e9d5 100644 --- a/jupyter_cache/cli/arguments.py +++ b/jupyter_cache/cli/arguments.py @@ -30,9 +30,17 @@ type=click.Path(dir_okay=False, exists=True, readable=True, resolve_path=True), ) +OUTPUT_PATH = click.argument( + "outpath", + metavar="OUTPUT_PATH", + type=click.Path(dir_okay=False, writable=True, resolve_path=True), +) + PK = click.argument("pk", metavar="ID", type=int) PKS = click.argument("pks", metavar="IDs", nargs=-1, type=int) +PK_OR_PATH = click.argument("pk_path", metavar="ID_OR_PATH", type=str) + PK_OR_PATHS = click.argument("pk_paths", metavar="ID_OR_PATHS", nargs=-1) diff --git a/jupyter_cache/cli/commands/cmd_cache.py b/jupyter_cache/cli/commands/cmd_cache.py index 3373d5a..ceef878 100644 --- a/jupyter_cache/cli/commands/cmd_cache.py +++ b/jupyter_cache/cli/commands/cmd_cache.py @@ -10,7 +10,7 @@ @jcache.group("cache") def cmnd_cache(): - """Commands for adding to and inspecting the cache.""" + """Commands for interacting with cached executions.""" pass diff --git a/jupyter_cache/cli/commands/cmd_project.py b/jupyter_cache/cli/commands/cmd_project.py index 2821f0e..f192771 100644 --- a/jupyter_cache/cli/commands/cmd_project.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -1,6 +1,7 @@ import sys import click +import nbformat from jupyter_cache import get_cache from jupyter_cache.cli import arguments, options @@ -58,12 +59,12 @@ def clear_nbs(cache_path, force): @arguments.PK_OR_PATHS @options.CACHE_PATH def remove_nbs(cache_path, pk_paths): - """Remove notebook(s) from the project, by ID/URI.""" + """Remove notebook(s) from the project (by ID/URI).""" db = get_cache(cache_path) - for path in pk_paths: + for pk_path in pk_paths: # TODO deal with errors (print all at end? or option to ignore) - click.echo("Removing: {}".format(path)) - db.remove_nb_from_project(path) + click.echo("Removing: {}".format(pk_path)) + db.remove_nb_from_project(int(pk_path) if pk_path.isdigit() else pk_path) click.secho("Success!", fg="green") @@ -95,7 +96,7 @@ def list_nbs_in_project(cache_path, path_length): help="Show traceback, if last execution failed.", ) def show_project_record(cache_path, pk, tb): - """Show details of a notebook.""" + """Show details of a notebook (by ID).""" import yaml db = get_cache(cache_path) @@ -115,3 +116,17 @@ def show_project_record(cache_path, pk, tb): click.secho("Failed Last Execution!", fg="red") if tb: click.echo(record.traceback) + + +@cmnd_project.command("merge") +@arguments.PK_OR_PATH +@arguments.OUTPUT_PATH +@options.CACHE_PATH +def merge_executed(cache_path, pk_path, outpath): + """Write notebook merged with cached outputs (by ID/URI).""" + db = get_cache(cache_path) + bundle = db.get_project_notebook(int(pk_path) if pk_path.isdigit() else pk_path) + cached_pk, nb = db.merge_match_into_notebook(bundle.nb) + nbformat.write(nb, outpath) + click.echo(f"Merged with cache PK {cached_pk}") + click.secho("Success!", fg="green") diff --git a/tests/make_cli_readme.py b/tests/make_cli_readme.py index cc9c0fa..a28c045 100644 --- a/tests/make_cli_readme.py +++ b/tests/make_cli_readme.py @@ -1,5 +1,7 @@ import os +import sys from datetime import datetime +from pathlib import Path from textwrap import dedent from click.testing import CliRunner @@ -253,8 +255,11 @@ def main(): ) strings.append(get_string(cmd_project.show_project_record, project_cmd_name, ["1"])) - return "\n\n".join(strings) + return "\n\n".join(strings).rstrip() + "\n" if __name__ == "__main__": - print(main()) + outpath = Path(sys.argv[1]) + output = main() + outpath.write_text(output) + print(f"Written to {outpath.absolute()}") diff --git a/tests/test_cli.py b/tests/test_cli.py index 454d4dc..6c39e83 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -174,6 +174,17 @@ def test_remove_nbs_from_project(tmp_path): assert db.list_project_records() == [] +def test_clear_project(tmp_path): + db = JupyterCacheBase(str(tmp_path)) + path = os.path.join(NB_PATH, "basic.ipynb") + runner = CliRunner() + result = runner.invoke(cmd_project.add_notebooks, ["-p", tmp_path, path]) + result = runner.invoke(cmd_project.clear_nbs, ["-p", tmp_path], input="y") + assert result.exception is None, result.output + assert result.exit_code == 0, result.output + assert db.list_project_records() == [] + + def test_list_nbs_in_project(tmp_path): db = JupyterCacheBase(str(tmp_path)) db.cache_notebook_file( @@ -201,3 +212,21 @@ def test_show_project_record(tmp_path): assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output + + +def test_project_merge(tmp_path): + db = JupyterCacheBase(str(tmp_path)) + record = db.add_nb_to_project(path=os.path.join(NB_PATH, "basic_unrun.ipynb")) + db.cache_notebook_file( + path=os.path.join(NB_PATH, "basic.ipynb"), + uri="basic.ipynb", + check_validity=False, + ) + runner = CliRunner() + result = runner.invoke( + cmd_project.merge_executed, + ["-p", tmp_path, str(record.pk), str(tmp_path / "output.ipynb")], + ) + assert result.exception is None, result.output + assert result.exit_code == 0, result.output + assert (tmp_path / "output.ipynb").exists() diff --git a/tox.ini b/tox.ini index 73dd7a5..169ff29 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ extras = cli deps = ipykernel jupytext -commands = python tests/make_cli_readme.py +commands = python tests/make_cli_readme.py docs/using/cli.md [testenv:docs-{clean,update}] extras = rtd From 86a57e1354af63ff434dff4d24bbdf7aafcda07a Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 3 Aug 2021 02:01:28 +0200 Subject: [PATCH 07/39] =?UTF-8?q?=F0=9F=93=9A=20DOCS:=20Update=20execution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .readthedocs.yml | 1 + docs/conf.py | 86 ++++++++++ docs/using/api.ipynb | 102 ++++++------ docs/using/cli.md | 326 +++++++++----------------------------- jupyter_cache/cache/db.py | 4 +- tests/make_cli_readme.py | 265 ------------------------------- tox.ini | 12 +- 7 files changed, 221 insertions(+), 575 deletions(-) delete mode 100644 tests/make_cli_readme.py diff --git a/.readthedocs.yml b/.readthedocs.yml index ccb61ea..2213067 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,6 +6,7 @@ python: - method: pip path: . extra_requirements: + - cli - rtd sphinx: diff --git a/docs/conf.py b/docs/conf.py index cade15e..92a8b1e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,3 +76,89 @@ ("py:class", "ForwardRef"), ("py:class", "NoneType"), ] + + +def setup(app): + import importlib + import os + import shutil + import traceback + + import click + from click.testing import CliRunner + from docutils import nodes + from docutils.parsers.rst import directives + from sphinx.util.docutils import SphinxDirective + + class JcacheClear(SphinxDirective): + def run(self): + path = os.path.join(os.path.dirname(self.env.app.srcdir), ".jupyter_cache") + if os.path.exists(path): + shutil.rmtree(path) + return [] + + class JcacheCli(SphinxDirective): + required_arguments = 1 # command + final_argument_whitespace = False + has_content = False + option_spec = { + "prog": directives.unchanged_required, + "command": directives.unchanged_required, + "args": directives.unchanged_required, + "input": directives.unchanged_required, + } + + def run(self): + modpath = self.arguments[0] + + try: + module_name, attr_name = modpath.split(":", 1) + except ValueError: + raise self.error(f'"{modpath}" is not of format "module:command"') + + try: + module = importlib.import_module(module_name) + except Exception: + raise self.error( + f"Failed to import '{module_name}': {traceback.format_exc()}" + ) + + if not hasattr(module, attr_name): + raise self.error( + f'Module "{module_name}" has no attribute "{attr_name}"' + ) + command = getattr(module, attr_name) + if not isinstance(command, click.Group): + raise self.error( + f'"{modpath}" of type {type(command)}"" is not derived from "click.Group"' + ) + + cmd_string = [self.options.get("prog", "jcache")] + if command.name != cmd_string[0]: + cmd_string.append(command.name) + if "command" in self.options: + cmd_string.append(self.options["command"]) + command = command.commands[self.options["command"]] + + args = self.options.get("args", "") + + runner = CliRunner() + root_path = os.path.dirname(self.env.app.srcdir) + try: + old_cwd = os.getcwd() + os.chdir(root_path) + result = runner.invoke( + command, args.split(), input=self.options.get("input", None), env={} + ) + finally: + os.chdir(old_cwd) + + text = f"$ {' '.join(cmd_string)} {args}\n{result.output}" + if result.exception: + text += "\n" + str(result.exception) + "\n" + text = text.replace(root_path + os.sep, "../") + node = nodes.literal_block(text, text, language="console") + return [node] + + app.add_directive("jcache-clear", JcacheClear) + app.add_directive("jcache-cli", JcacheCli) diff --git a/docs/using/api.ipynb b/docs/using/api.ipynb index 405afdf..74b8642 100644 --- a/docs/using/api.ipynb +++ b/docs/using/api.ipynb @@ -82,7 +82,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - "JupyterCacheBase('/Users/cjs14/GitHub/jupyter-cache/docs/using/.jupyter_cache')" + "JupyterCacheBase('/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/.jupyter_cache')" ] }, "metadata": {}, @@ -173,13 +173,13 @@ "output_type": "execute_result", "data": { "text/plain": [ - "{'data': {},\n", - " 'pk': 1,\n", - " 'uri': 'example_nbs/basic.ipynb',\n", - " 'accessed': datetime.datetime(2020, 3, 13, 14, 21, 46, 271953),\n", + "{'pk': 1,\n", + " 'hashkey': '94c17138f782c75df59e989fffa64e3a',\n", " 'description': '',\n", - " 'hashkey': '818f3412b998fcf4fe9ca3cca11a3fc3',\n", - " 'created': datetime.datetime(2020, 3, 13, 14, 21, 46, 271943)}" + " 'created': datetime.datetime(2021, 8, 2, 20, 39, 31, 350600),\n", + " 'data': {},\n", + " 'uri': 'example_nbs/basic.ipynb',\n", + " 'accessed': datetime.datetime(2021, 8, 2, 20, 39, 31, 350606)}" ] }, "metadata": {}, @@ -332,9 +332,9 @@ "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mCachingError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m record = cache.cache_notebook_file(\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mpath\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"example_nbs\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"basic.ipynb\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m )\n", - "\u001b[0;32m~/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_file\u001b[0;34m(self, path, uri, artifacts, data, check_validity, overwrite)\u001b[0m\n\u001b[1;32m 271\u001b[0m ),\n\u001b[1;32m 272\u001b[0m \u001b[0mcheck_validity\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcheck_validity\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 273\u001b[0;31m \u001b[0moverwrite\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0moverwrite\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 274\u001b[0m )\n\u001b[1;32m 275\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_bundle\u001b[0;34m(self, bundle, check_validity, overwrite, description)\u001b[0m\n\u001b[1;32m 208\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0moverwrite\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 209\u001b[0m raise CachingError(\n\u001b[0;32m--> 210\u001b[0;31m \u001b[0;34m\"Notebook already exists in cache and overwrite=False.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 211\u001b[0m )\n\u001b[1;32m 212\u001b[0m \u001b[0mshutil\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrmtree\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mparent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_34718/3576020660.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m record = cache.cache_notebook_file(\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"example_nbs\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"basic.ipynb\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m )\n", + "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_file\u001b[0;34m(self, path, uri, artifacts, data, check_validity, overwrite)\u001b[0m\n\u001b[1;32m 267\u001b[0m \"\"\"\n\u001b[1;32m 268\u001b[0m \u001b[0mnotebook\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnbf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnbf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mNO_CONVERT\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 269\u001b[0;31m return self.cache_notebook_bundle(\n\u001b[0m\u001b[1;32m 270\u001b[0m NbBundleIn(\n\u001b[1;32m 271\u001b[0m \u001b[0mnotebook\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_bundle\u001b[0;34m(self, bundle, check_validity, overwrite, description)\u001b[0m\n\u001b[1;32m 212\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexists\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 213\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0moverwrite\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 214\u001b[0;31m raise CachingError(\n\u001b[0m\u001b[1;32m 215\u001b[0m \u001b[0;34m\"Notebook already exists in cache and overwrite=False.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 216\u001b[0m )\n", "\u001b[0;31mCachingError\u001b[0m: Notebook already exists in cache and overwrite=False." ] } @@ -355,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "source": [ "notebook = nbf.read(str(Path(\"example_nbs\", \"basic.ipynb\")), 4)\n", "notebook" @@ -396,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "source": [ "cache.match_cache_notebook(notebook)" ], @@ -423,7 +423,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "source": [ "notebook.cells[0].source = \"change some text\"" ], @@ -432,7 +432,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "source": [ "cache.match_cache_notebook(notebook)" ], @@ -459,7 +459,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "source": [ "notebook.cells[1].source = \"change some source code\"" ], @@ -468,7 +468,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "source": [ "cache.match_cache_notebook(notebook)" ], @@ -502,7 +502,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "source": [ "print(cache.diff_nbnode_with_cache(1, notebook, as_str=True))" ], @@ -554,7 +554,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "source": [ "nb_bundle = NbBundleIn(\n", " nb=notebook,\n", @@ -579,7 +579,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "source": [ "print(tabulate_cache_records(\n", " cache.list_cache_records(), path_length=1, hashkeys=True\n", @@ -609,7 +609,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "source": [ "cache.get_cache_limit()" ], @@ -629,7 +629,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "source": [ "cache.change_cache_limit(100)" ], @@ -659,7 +659,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "source": [ "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", "record" @@ -680,7 +680,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "source": [ "record.to_dict()" ], @@ -711,7 +711,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "source": [ "cache.get_cached_project_nb(1)" ], @@ -731,7 +731,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -766,7 +766,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "source": [ "cache.merge_match_into_file(\n", " cache.get_project_record(1).uri,\n", @@ -818,7 +818,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "source": [ "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic_failing.ipynb\"))\n", "record" @@ -839,7 +839,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "source": [ "cache.get_cached_project_nb(2) # returns None" ], @@ -848,7 +848,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "source": [ "cache.list_unexecuted()" ], @@ -868,7 +868,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -897,7 +897,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "source": [ "cache.remove_nb_from_project(1)" ], @@ -906,7 +906,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -943,7 +943,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "source": [ "cache.clear_cache()\n", "cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", @@ -965,7 +965,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -999,7 +999,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "source": [ "list_executors()" ], @@ -1019,7 +1019,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "source": [ "from logging import basicConfig, INFO\n", "basicConfig(level=INFO)\n", @@ -1066,7 +1066,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "source": [ "result = executor.run_and_cache()\n", "result" @@ -1106,7 +1106,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "source": [ "cache.list_cache_records()" ], @@ -1126,7 +1126,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "source": [ "record = cache.get_cache_record(1)\n", "record.to_dict()" @@ -1162,7 +1162,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": null, "source": [ "record = cache.get_project_record(2)\n", "print(record.traceback)" @@ -1211,7 +1211,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -1233,7 +1233,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": null, "source": [ "print(tabulate_cache_records(\n", " cache.list_cache_records(), path_length=1, hashkeys=True\n", @@ -1274,21 +1274,23 @@ "metadata": { "celltoolbar": "Tags", "kernelspec": { - "display_name": "Python 3.7.6 64-bit ('mistune': conda)", - "language": "python", - "name": "python37664bitmistuneconda77ae93e05d9c4c1eab3d7fc3f8312065" + "name": "python3", + "display_name": "Python 3.8.6 64-bit (conda)" + }, + "interpreter": { + "hash": "fb9a14a894377db1cfdad9c4af43de04ce5a0728c4aa470d5611ebb394042755" }, "language_info": { + "name": "python", + "version": "3.8.10", + "mimetype": "text/x-python", "codemirror_mode": { "name": "ipython", "version": 3 }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "nbconvert_exporter": "python", + "file_extension": ".py" } }, "nbformat": 4, diff --git a/docs/using/cli.md b/docs/using/cli.md index ffccde0..e19cc9b 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -2,28 +2,20 @@ # Command-Line - +```{jcache-clear} +``` From the checked-out repository folder: -```console -$ jcache --help -Usage: jcache [OPTIONS] COMMAND [ARGS]... - - The command line interface of jupyter-cache. - -Options: - -v, --version Show the version and exit. - -p, --cache-path Print the current cache path and exit. - -a, --autocomplete Print the autocompletion command and exit. - -h, --help Show this message and exit. - -Commands: - cache Commands for interacting with cached executions. - clear Clear the cache completely. - config Commands for configuring the cache. - execute Execute all or specific outdated notebooks in the project. - project Commands for interacting with a project. +```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +:args: --help +``` + +The first time the cache is required, it will be lazily created: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: list +:input: y ``` ````{tip} @@ -36,58 +28,31 @@ eval "$(_JCACHE_COMPLETE=source jcache)" ## Caching Executed Notebooks -```console -$ jcache cache --help -Usage: cache [OPTIONS] COMMAND [ARGS]... - - Commands for interacting with cached executions. - -Options: - --help Show this message and exit. - -Commands: - add Cache notebook(s) that have already been executed. - add-with-artefacts Cache a notebook, with possible artefact files. - cat-artifact Print the contents of a cached artefact. - clear Remove all executed notebooks from the cache. - diff-nb Print a diff of a notebook to one stored in the cache. - list List cached notebook records in the cache. - remove Remove notebooks stored in the cache. - show Show details of a cached notebook in the cache. +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:args: --help ``` -The first time the cache is required, it will be lazily created: - -```console -$ jcache cache list -Cache path: ../.jupyter_cache -The cache does not yet exist, do you want to create it? [y/N]: y -No Cached Notebooks +Initially there will be no cached notebooks: +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: list ``` You can add notebooks straight into the cache. When caching, a check will be made that the notebooks look to have been executed correctly, i.e. the cell execution counts go sequentially up from 1. -```console -$ jcache cache add tests/notebooks/basic.ipynb -Caching: ../tests/notebooks/basic.ipynb -Validity Error: Expected cell 1 to have execution_count 1 not 2 -The notebook may not have been executed, continue caching? [y/N]: y -Success! +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: add +:args: tests/notebooks/basic.ipynb +:input: y ``` Or to skip validation: -```console -$ jcache cache add --no-validate tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb -Caching: ../tests/notebooks/basic.ipynb -Caching: ../tests/notebooks/basic_failing.ipynb -Caching: ../tests/notebooks/basic_unrun.ipynb -Caching: ../tests/notebooks/complex_outputs.ipynb -Caching: ../tests/notebooks/external_output.ipynb -Success! +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: add +:args: --no-validate tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb ``` Once you've cached some notebooks, you can look at the 'cache records' @@ -101,14 +66,8 @@ Multiple hashes for the same URI can be added the last accessed records begin to be deleted. You can remove cached records by their ID. -```console -$ jcache cache list - ID Origin URI Created Accessed ----- ------------------------------------- ---------------- ---------------- - 5 tests/notebooks/external_output.ipynb 2021-08-02 06:06 2021-08-02 06:06 - 4 tests/notebooks/complex_outputs.ipynb 2021-08-02 06:06 2021-08-02 06:06 - 3 tests/notebooks/basic_unrun.ipynb 2021-08-02 06:06 2021-08-02 06:06 - 2 tests/notebooks/basic_failing.ipynb 2021-08-02 06:06 2021-08-02 06:06 +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: list ``` ````{tip} @@ -122,99 +81,51 @@ $ jcache cache list --latest-only You can also cache notebooks with artefacts (external outputs of the notebook execution). -```console -$ jcache cache add-with-artefacts -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt -Caching: ../tests/notebooks/basic.ipynb -Validity Error: Expected cell 1 to have execution_count 1 not 2 -The notebook may not have been executed, continue caching? [y/N]: y -Success! +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: add-with-artefacts +:args: --no-validate -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt +:input: y ``` Show a full description of a cached notebook by referring to its ID -```console -$ jcache cache show 6 -ID: 6 -Origin URI: ../tests/notebooks/basic.ipynb -Created: 2021-08-02 06:06 -Accessed: 2021-08-02 06:06 -Hashkey: 94c17138f782c75df59e989fffa64e3a -Artifacts: -- artifact_folder/artifact.txt +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: show +:args: 6 ``` Note artefact paths must be 'upstream' of the notebook folder: -```console -$ jcache cache add-with-artefacts -nb tests/notebooks/basic.ipynb tests/test_db.py -Caching: ../tests/notebooks/basic.ipynb -Artifact Error: Path '../tests/test_db.py' is not in folder '../tests/notebooks'' +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: add-with-artefacts +:args: -nb tests/notebooks/basic.ipynb tests/test_db.py ``` To view the contents of an execution artefact: -```console -$ jcache cache cat-artifact 6 artifact_folder/artifact.txt -An artifact - +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: cat-artifact +:args: 6 artifact_folder/artifact.txt ``` You can directly remove a cached notebook by its ID: -```console -$ jcache cache remove 4 -Removing Cache ID = 4 -Success! +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: remove +:args: 4 ``` You can also diff any of the cached notebooks with any (external) notebook: -```console -$ jcache cache diff-nb 2 tests/notebooks/basic.ipynb -nbdiff ---- cached pk=2 -+++ other: ../tests/notebooks/basic.ipynb -## inserted before nb/cells/0: -+ code cell: -+ execution_count: 2 -+ source: -+ a=1 -+ print(a) -+ outputs: -+ output 0: -+ output_type: stream -+ name: stdout -+ text: -+ 1 - -## deleted nb/cells/0: -- code cell: -- source: -- raise Exception('oopsie!') - - -Success! +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: diff-nb +:args: 2 tests/notebooks/basic.ipynb ``` ## Adding notebooks to the project -```console -$ jcache project --help -Usage: project [OPTIONS] COMMAND [ARGS]... - - Commands for interacting with a project. - -Options: - --help Show this message and exit. - -Commands: - add Add notebook(s) to the project. - add-with-assets Add notebook(s) to the project, with possible asset files. - clear Remove all notebooks from the project. - list List notebooks in the project. - merge Write notebook merged with cached outputs (by ID/URI). - remove Remove notebook(s) from the project (by ID/URI). - show Show details of a notebook (by ID). +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:args: --help ``` A project consist of a set of notebooks to be executed. @@ -225,131 +136,53 @@ i.e. no physical copying takes place until execution time. You can list the notebooks to see which have existing records in the cache (by hash), and which will require execution: -```console -$ jcache project add tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb -Adding: ../tests/notebooks/basic.ipynb -Adding: ../tests/notebooks/basic_failing.ipynb -Adding: ../tests/notebooks/basic_unrun.ipynb -Adding: ../tests/notebooks/complex_outputs.ipynb -Adding: ../tests/notebooks/external_output.ipynb -Success! +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: add +:args: tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb ``` -```console -$ jcache project list - ID URI Reader Created Assets Cache ID ----- ------------------------------------- -------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 06:06 0 5 - 4 tests/notebooks/complex_outputs.ipynb nbformat 2021-08-02 06:06 0 - 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 06:06 0 6 - 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 06:06 0 2 - 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 06:06 0 6 +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: list ``` You can remove a notebook from the project by its URI or ID: -```console -$ jcache project remove 4 -Removing: 4 -Success! +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: remove +:args: 4 ``` You can then run a basic execution of the required notebooks: -```console -$ jcache cache remove 6 2 -Removing Cache ID = 6 -Removing Cache ID = 2 -Success! +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: remove +:args: 6 2 ``` -```console -$ jcache execute -Executing: ../tests/notebooks/basic.ipynb -Execution Succeeded: ../tests/notebooks/basic.ipynb -Executing: ../tests/notebooks/basic_failing.ipynb -error: Execution Failed: ../tests/notebooks/basic_failing.ipynb -Executing: ../tests/notebooks/basic_unrun.ipynb -Execution Succeeded: ../tests/notebooks/basic_unrun.ipynb -Finished! Successfully executed notebooks have been cached. -succeeded: -- ../tests/notebooks/basic.ipynb -- ../tests/notebooks/basic_unrun.ipynb -excepted: -- ../tests/notebooks/basic_failing.ipynb -errored: [] -up-to-date: [] - +```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +:command: execute ``` Successfully executed notebooks will be cached to the cache, along with any 'artefacts' created by the execution, that are inside the notebook folder, and data supplied by the executor. -```console -$ jcache project list - ID URI Reader Created Assets Cache ID ----- ------------------------------------- -------- ---------------- -------- ---------- - 5 tests/notebooks/external_output.ipynb nbformat 2021-08-02 06:06 0 5 - 3 tests/notebooks/basic_unrun.ipynb nbformat 2021-08-02 06:06 0 6 - 2 tests/notebooks/basic_failing.ipynb nbformat 2021-08-02 06:06 0 - 1 tests/notebooks/basic.ipynb nbformat 2021-08-02 06:06 0 6 +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: list ``` Execution data (such as execution time) will be stored in the cache record: -```console -$ jcache cache show 6 -ID: 6 -Origin URI: ../tests/notebooks/basic_unrun.ipynb -Created: 2021-08-02 06:06 -Accessed: 2021-08-02 06:06 -Hashkey: 94c17138f782c75df59e989fffa64e3a -Data: - execution_seconds: 0.887201879 - +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: show +:args: 6 ``` Failed notebooks will not be cached, but the exception traceback will be added to the notebook's project record: -```console -$ jcache project show 2 -ID: 2 -URI: ../tests/notebooks/basic_failing.ipynb -Reader: nbformat -Created: 2021-08-02 06:06 -Failed Last Execution! -Traceback (most recent call last): - File "../jupyter_cache/executors/utils.py", line 55, in single_nb_execution - record_timing=False, - File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/client.py", line 1112, in execute - return NotebookClient(nb=nb, resources=resources, km=km, **kwargs).execute() - File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/util.py", line 74, in wrapped - return just_run(coro(*args, **kwargs)) - File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/util.py", line 53, in just_run - return loop.run_until_complete(coro) - File "../.tox/create_cli_doc/lib/python3.7/asyncio/base_events.py", line 587, in run_until_complete - return future.result() - File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/client.py", line 554, in async_execute - cell, index, execution_count=self.code_cells_executed + 1 - File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/client.py", line 857, in async_execute_cell - self._check_raise_for_error(cell, exec_reply) - File "../.tox/create_cli_doc/lib/python3.7/site-packages/nbclient/client.py", line 760, in _check_raise_for_error - raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content) -nbclient.exceptions.CellExecutionError: An error occurred while executing the following cell: ------------------- -raise Exception('oopsie!') ------------------- - ---------------------------------------------------------------------------- -Exception Traceback (most recent call last) -/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_3944/340246212.py in -----> 1 raise Exception('oopsie!') - -Exception: oopsie! -Exception: oopsie! - - +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: show +:args: 2 ``` ```{tip} @@ -359,10 +192,9 @@ Code cells can be tagged with `raises-exception` to let the executor known that Once executed you may leave notebooks in the project, for later re-execution, or remove them: -```console -$ jcache project clear -Are you sure you want to permanently clear the project!? [y/N]: y -Project cleared! +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: clear +:input: y ``` You can also add notebooks to the projects with assets; @@ -370,18 +202,12 @@ external files that are required by the notebook during execution. As with artefacts, these files must be in the same folder as the notebook, or a sub-folder. -```console -$ jcache project add-with-assets -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt -Success! +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: add-with-assets +:args: -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt ``` -```console -$ jcache project show 1 -ID: 1 -URI: ../tests/notebooks/basic.ipynb -Reader: nbformat -Created: 2021-08-02 06:06 -Cache ID: 6 -Assets: -- ../tests/notebooks/artifact_folder/artifact.txt +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: show +:args: 1 ``` diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 1cd1272..2e6d1f7 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -17,7 +17,9 @@ # TODO store this in the database so we can check for updates DB_VERSION = 2 -# v2: nbstage -> nbproject, and added reader field to nbproject +# v2: +# - table: nbstage -> nbproject +# - added reader field to nbproject def create_db(path, name="global.db") -> Engine: diff --git a/tests/make_cli_readme.py b/tests/make_cli_readme.py deleted file mode 100644 index a28c045..0000000 --- a/tests/make_cli_readme.py +++ /dev/null @@ -1,265 +0,0 @@ -import os -import sys -from datetime import datetime -from pathlib import Path -from textwrap import dedent - -from click.testing import CliRunner - -from jupyter_cache.cache.main import DEFAULT_CACHE_LIMIT -from jupyter_cache.cli.commands import cmd_cache, cmd_exec, cmd_main, cmd_project - - -def get_string(cli, group=None, args=(), input=None): - command_str = ["jcache"] if cli.name != "jcache" else [] - if group: - command_str.append(group) - command_str.append(cli.name) - command_str = " ".join(command_str) - - runner = CliRunner() - result = runner.invoke(cli, args, input=input) - root_path = os.getcwd() + os.sep - output = result.output.replace(root_path, "../") - if result.exception: - output += "\n" + str(result.exception) + "\n" - return "```console\n$ {}{}\n{}```".format( - command_str, - (" " + " ".join(args)) if args else "", - output, - ) - - -def main(): - - get_string(cmd_main.clear_cache, input="y") - - strings = ["(use/cli)=", "# Command-Line"] - strings.append( - "".format( - datetime.now().isoformat(" ", "minutes"), __file__ - ) - ) - strings.append("From the checked-out repository folder:") - strings.append(get_string(cmd_main.jcache, None, ["--help"])) - strings.append( - dedent( - """\ - ````{tip} - Execute this in the terminal for auto-completion: - - ```console - eval "$(_JCACHE_COMPLETE=source jcache)" - ``` - ````""" - ) - ) - - # cache - strings.append("## Caching Executed Notebooks") - cache_name = cmd_cache.cmnd_cache.name - strings.append(get_string(cmd_cache.cmnd_cache, None, ["--help"])) - strings.append("The first time the cache is required, it will be lazily created:") - strings.append(get_string(cmd_cache.list_caches, cache_name, input="y")) - strings.append( - dedent( - """\ - You can add notebooks straight into the cache. - When caching, a check will be made that the notebooks look to have been executed - correctly, i.e. the cell execution counts go sequentially up from 1.""" - ) - ) - strings.append( - get_string( - cmd_cache.cache_nbs, cache_name, ["tests/notebooks/basic.ipynb"], input="y" - ) - ) - strings.append("Or to skip validation:") - strings.append( - get_string( - cmd_cache.cache_nbs, - cache_name, - [ - "--no-validate", - "tests/notebooks/basic.ipynb", - "tests/notebooks/basic_failing.ipynb", - "tests/notebooks/basic_unrun.ipynb", - "tests/notebooks/complex_outputs.ipynb", - "tests/notebooks/external_output.ipynb", - ], - ) - ) - strings.append( - dedent( - """\ - Once you've cached some notebooks, you can look at the 'cache records' - for what has been cached. - - Each notebook is hashed (code cells and kernel spec only), - which is used to compare against notebooks in the project. - Multiple hashes for the same URI can be added - (the URI is just there for inspection) and the size of the cache is limited - (current default {}) so that, at this size, - the last accessed records begin to be deleted. - You can remove cached records by their ID.""".format( - DEFAULT_CACHE_LIMIT - ) - ) - ) - strings.append(get_string(cmd_cache.list_caches, cache_name)) - strings.append( - dedent( - """\ - ````{tip} - To only show the latest versions of cached notebooks. - - ```console - $ jcache cache list --latest-only - ``` - ````""" - ) - ) - strings.append( - dedent( - """\ - You can also cache notebooks with artefacts - (external outputs of the notebook execution).""" - ) - ) - strings.append( - get_string( - cmd_cache.cache_nb, - cache_name, - [ - "-nb", - "tests/notebooks/basic.ipynb", - "tests/notebooks/artifact_folder/artifact.txt", - ], - input="y", - ) - ) - strings.append( - "Show a full description of a cached notebook by referring to its ID" - ) - strings.append(get_string(cmd_cache.show_cache, cache_name, ["6"])) - strings.append("Note artefact paths must be 'upstream' of the notebook folder:") - strings.append( - get_string( - cmd_cache.cache_nb, - cache_name, - ["-nb", "tests/notebooks/basic.ipynb", "tests/test_db.py"], - ) - ) - strings.append("To view the contents of an execution artefact:") - strings.append( - get_string( - cmd_cache.cat_artifact, cache_name, ["6", "artifact_folder/artifact.txt"] - ) - ) - strings.append("You can directly remove a cached notebook by its ID:") - strings.append(get_string(cmd_cache.remove_caches, cache_name, ["4"])) - strings.append( - "You can also diff any of the cached notebooks with any (external) notebook:" - ) - strings.append( - get_string(cmd_cache.diff_nb, cache_name, ["2", "tests/notebooks/basic.ipynb"]) - ) - - strings.append("## Adding notebooks to the project") - project_cmd_name = cmd_project.cmnd_project.name - strings.append(get_string(cmd_project.cmnd_project, None, ["--help"])) - strings.append( - dedent( - """\ - A project consist of a set of notebooks to be executed. - - Notebooks are recorded as pointers to their URI (e.g. file path), - i.e. no physical copying takes place until execution time. - - You can list the notebooks to see which have existing records in the cache (by hash), - and which will require execution:""" - ) - ) - strings.append( - get_string( - cmd_project.add_notebooks, - project_cmd_name, - [ - "tests/notebooks/basic.ipynb", - "tests/notebooks/basic_failing.ipynb", - "tests/notebooks/basic_unrun.ipynb", - "tests/notebooks/complex_outputs.ipynb", - "tests/notebooks/external_output.ipynb", - ], - ) - ) - strings.append(get_string(cmd_project.list_nbs_in_project, project_cmd_name)) - strings.append("You can remove a notebook from the project by its URI or ID:") - strings.append(get_string(cmd_project.remove_nbs, project_cmd_name, ["4"])) - strings.append("You can then run a basic execution of the required notebooks:") - strings.append(get_string(cmd_cache.remove_caches, cache_name, ["6", "2"])) - strings.append(get_string(cmd_exec.execute_nbs, None)) - strings.append( - dedent( - """\ - Successfully executed notebooks will be cached to the cache, - along with any 'artefacts' created by the execution, - that are inside the notebook folder, and data supplied by the executor.""" - ) - ) - strings.append(get_string(cmd_project.list_nbs_in_project, project_cmd_name)) - strings.append( - "Execution data (such as execution time) will be stored in the cache record:" - ) - strings.append(get_string(cmd_cache.show_cache, cache_name, ["6"])) - strings.append( - "Failed notebooks will not be cached, " - "but the exception traceback will be added to the notebook's project record:" - ) - strings.append(get_string(cmd_project.show_project_record, project_cmd_name, ["2"])) - strings.append( - dedent( - """\ - ```{tip} - Code cells can be tagged with `raises-exception` to let the executor known that a cell *may* raise an exception - (see [this issue on its behaviour](https://github.com/jupyter/nbconvert/issues/730)). - ```""" # noqa: E501 - ) - ) - strings.append( - "Once executed you may leave notebooks in the project, " - "for later re-execution, or remove them:" - ) - strings.append(get_string(cmd_project.clear_nbs, project_cmd_name, [], input="y")) - - # assets - strings.append( - dedent( - """\ - You can also add notebooks to the projects with assets; - external files that are required by the notebook during execution. - As with artefacts, these files must be in the same folder as the notebook, - or a sub-folder.""" - ) - ) - strings.append( - get_string( - cmd_project.add_notebook, - project_cmd_name, - [ - "-nb", - "tests/notebooks/basic.ipynb", - "tests/notebooks/artifact_folder/artifact.txt", - ], - ) - ) - strings.append(get_string(cmd_project.show_project_record, project_cmd_name, ["1"])) - - return "\n\n".join(strings).rstrip() + "\n" - - -if __name__ == "__main__": - outpath = Path(sys.argv[1]) - output = main() - outpath.write_text(output) - print(f"Written to {outpath.absolute()}") diff --git a/tox.ini b/tox.ini index 169ff29..0bb66b1 100644 --- a/tox.ini +++ b/tox.ini @@ -30,16 +30,10 @@ deps = jupytext commands = jcache {posargs} -[testenv:create_cli_doc] -description = Create output to add to docs/using/cli.md -extras = cli -deps = - ipykernel - jupytext -commands = python tests/make_cli_readme.py docs/using/cli.md - [testenv:docs-{clean,update}] -extras = rtd +extras = + cli + rtd whitelist_externals = echo rm From 35190654a08c4123cff727471acc65e22a8f0919 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 3 Aug 2021 17:41:59 +0200 Subject: [PATCH 08/39] =?UTF-8?q?=F0=9F=93=9A=20DOCS:=20Update=20logo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/_static/logo.jpg | Bin 465644 -> 0 bytes docs/_static/logo_small.jpg | Bin 8158 -> 0 bytes docs/_static/logo_square.svg | 4 ++++ docs/_static/logo_wide.svg | 6 ++++++ docs/conf.py | 5 ++++- 5 files changed, 14 insertions(+), 1 deletion(-) delete mode 100644 docs/_static/logo.jpg delete mode 100644 docs/_static/logo_small.jpg create mode 100644 docs/_static/logo_square.svg create mode 100644 docs/_static/logo_wide.svg diff --git a/docs/_static/logo.jpg b/docs/_static/logo.jpg deleted file mode 100644 index fd9f578484a4a0d523bd102ade2f76cc5b9a0a4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 465644 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%0Da`(LUh)xUL~MH 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

LX_k#&9KfCRopJ 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%0DL}4mfzGte=6_dBjRs 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ΞAC*A}dxmav7LKzaGgoVG@p}mR}}J_bvYcMfVKJY#?_0VBlrV zAkFzR5$kP$-Ds5G@Fv=B8eGHrQ8gZ%ZWV-7pof!3umD&Z+_AV4BLZ|8-MlCOtd&!- ziD^oDXVr#e=k^~xK9x>B_l8%`Ckhja+DNF(?k4+bJ6BDKwj}S*+|&Z+=(||YF(}+n z&lMQi9hl-@Fa@>|_b14hvoX3%GirrH>(~ng`A}2P2_s*%f}zHWJyhPiv{B(JWiN7)s(bVa6DGa=1B%dWfbO>tPXrFn#Nrx@ppfb8!8Ets_|P##xMF5D@#nh?DW5m zRUP7->kDpbnFz?aUtru!XcWnrt*IGu`Gc6oR1lVVs%u)hOP3v535vHK0ZkWv7t#r= z7ihgu(CyHoM53VnVNxUJ%C=CPZsek%cB1zfxJU3O(qOda;@sjwuHWZ`r_wov`j3K) z)R>gBu_8g2Z8mJ)pO8#^gwXkRrC=1zJIsv~ABhp%&BsaD6=;9nq-7Hp+$e6g`Tlqs znHTnp1zRAJ^~#uUof63a;cSs+m~mlT@w)w^mmeKe(%<(Sonhl?;-X9qBc&9Hjo&dR zZQLwOH0>H7&QfC2q_rQQdXC=q`f6auH#=z&{m2OvJn_wlOEnUtSVeZ*5D;LZjJI#v zG&CXKejfa%b)?ZIRM^TQAX1b*1iQWsKIHa2lcYHogCR`5yBx0|GCE>sY71OUF#BcE z@XS@!n;M!LvN4oGw3ZH>5j~4Q|0do(=JKG)*f!(z?a$iq*gUAvBXks z5YZ=v9DpLK7)>!fhvVObcp9jeeDgdNnU%8s_Ir*)Z`{EA$@$lMH2kOiN!6R?oXqoe zvwfz+KyHq6wZaLP&-X5?^^Gff6uP)$nQnTOpN!LUI;@%&wQ*Od&9-t>WR}7}R zlIat^;~Cn0!@u<6anfVEHOD_%0-;?6k7nF0%zcSpV^P4CcFJ_&kpp_$kB6o-rdeDA zoSzGU9Lw8|LI0EfhECsG{4$5#+s-g z?u2n3O~~}Qlp)eMU3N2psnGg^<^R;Axv1I+9sC8gIjXY7WAL87`U8^vi%f?_G~}0! zxTpdu{NNq)Q>cyIaP&_(3-5ty)Oyn~8?h3W-KW5;#!b3uWtoPE50Cfr!fE1Uv1#V? zs&#s-LxZ~WwCmz=zW?bW^Km}eFaH(%_GJV2UQO|Bf(qq+@fg+!)-9R%wi8+fp_QuBxXW(XG%xttOfmHtG@=dr}lQhvx;(`;K7Yp@kAH}oZpgG|U*ipDWof(SxmB4`Nd#j7TE$x-Cp z|Gu&NKzikuLeMq*&Fig@l(z38HAdq1?226`>b#vGUEKMHSbt6VBLrU_KGcD5|8VjhUD8x9jCt=SA^0@%WtxDDjcdkQ8^3X-OE>Ip|LnR5q==kJWjt zaqVQqjTRY8Yd>StP+vJpC}1FP{@m`On0_4gF6a}2d0l7|QpuYFT>)kQ*N8=Y zXZ#`Uilln}@-gW;h(KhR9mDPxCDHPYWs5Q=5l1-wzWc(#gs;1kmlZ9sD8P)?;X12l zW~(3VP1*nB7M9-(F) z>^)PDfA}WA#wwyKT2wAIgvPJ`oMKjvCdF9Ni1^-NxQ?8jP%*Y59ayoVfrgytyV1dH zRUV<}RL!CC(dl+<{n5Dci9EZsyICVlx9+TNw;=gBzJW82Cx8AGaV;>HOA;}7rrf7l zAveDg?Hp-^by?$x_wQ3Zwbf{J-MXwuF&=ejIrFeMN4x%gGPedkf~}pU(c}V zMwA)}ri6(ge~KIEvrIecnh%A6Ne=UxLb5-tQYTiN%&Zd_nF_@3{;P!75vmlevHSmQ+1VpbjC&_C(ul3oA zVxRX~IotPF;wnxy@RumGd3`7*%dt~acM%T$x+r6xoPqzbO7PpD5P71bHlBjtoWdvc zQb_Y(nGWV#co^Lne5XepXarUB#KnlskVC zQ`t-Tc=p6XIV29zo>?^tH!+*1e)`c8gFBX)ML?rKIn<_B)d_nI;pmzm^aSPxG3V-u z6yptxd!!nO%FD52;iJpc|_91zaoJ(5tK|ETinb4${jzE!N$pnj68#FI#Pd-H7C zDq~KBGgdc^#+A2k51UVvrM8!^L`U<$c-YU`xE_6h0!({uN>Nye{KW@@Z|Tz4@S>sX z1dN2>JTgAVEYddaq{pwo$1VN2ra53&lXx_5tKi{wjA{C!R(b8Ni%T)TvYFoxH665|mJKJ8|x`A@q5Vgz$#BR!xE^XfX`zg}c@>X42PelS!-_A;&va z&rS!wPf?QY9O_$K@0qihZ9xcq9+2L;`0k~2 z;XjNcXa}Rzbt&0q59QN(SDet<-1~ZvW{RPsRhi7av;44m#o_7cOzh<(Vi=MZG2J3&Z?kMQ!z{9*F=an68H)~a>> zn)NB`oMw$^|7pCV29wy!Vnuv4&wq*&k*Ta(wak;s<0h!!b{ktE&^X*p`5%zmn`x$E&DX_M;SHHpH2OAoln}nLWs+e#Ul{Mi#vAfx1wDT8IZkX`)LKRXMsR2n0BuEXS#WpF8Uta|uMN9lK!(qFdQ@lss${0U zu;U}`v%lWY0>l3e6xCsaX?)YVVSGPxMUH4tA!lI=Pt|k(-l>Kg;$S_WQ|c09Ve|e$ zP*o}-juT$mSGu7d79c+=*V;IlRs6(W;=s!LN3b)|Ec>Y^nv4&(%x`yeD?dV6u60&d z6TR>W4b$sTu1TcSCG~wz|GhVV{^e^aqF3$5Z3Ec)Jk@mZJ9@HwyP{LX%ouDxzEnR7 z*^Q2H+G3yQ7P)y@i7UAO{Q`?$3AWw&<-X1>gmrM`<%lND7eny0<6 z3d+kA^(V^<+rQshsX&x6g9=*+O-E6it>2s;_9c#oq*j4~7RCHjE zx?(@X08~V06e51DKuwN&*~M}QT~mFKTPz}&BIh9d%r`sj947e8SH6#;xh!4J4>67| zD+7XAPrc?#WGWZ0f310;w@$N#kx#|&oBD>-MTwWK?28{CZ8x_b^t1hy7#NdlnmG$Q z=mHL7&26%g^U|CPw7nrFzg}imGPR_gkd=6(ID3>xvSUpQRAis~yV)zo%wM@mifw-ioSOodtqS8CqX$0OBEwtB->pa_lULXHRnC1}5m|XHF6}o>; zKbOEYm0EPLTk$CqHf}d78?0Y#;I47OrbW1B;!%%?ubEPD?ayR`%})hIt3}@(CMYQ3 zX}`GBOkrTgk>KX*t4WBTim{{m!>`B21+AA@UW)Eiy3AvN7TbEHf8j@Z62B|3LEjek zvr&>6(-yvwp@4gDP2y(``NE35LRb4_#^VAV)dC7&_0Q8^8H)*B@Cz*F;U@hQlTfz0 z0CK&p`Qp?-{>Paxg!y~@_X1qD?SLx}@D7;RtKN*4L@ekP5OUW4qhMW3@qM&cX&-C2t z{NAA&xilQ_ZcTaLi^Is7!$KV^kSiO{E2{ub0)8wd@TX)9sUw8GFefQ8jJ<)W|WB}@?O>}SM z#XXePRk3~zrJ&)HwfHhPlmRLN6HIxhy10d{-lyS9e&-hjx*c21tw!Zs#4RRuH3Ul& zY6O4ZkCB(`z0*{r`#iP1^|8^hCxNy8^&4FS(LrDfC&(cEd#hib`=rgyk&JXdgO*_q z0WIjxK)AR978l}0;0DMhHWD|viwaKodF;<3@rb3r$NaleiYiIx9A@=~i;&SXy_5J{ zpSn|TKAn4&b{ya7S$OXGZ$eODRYFFY+*I@_)!0866u?z`^GZ6zRJKoFv{};Dd{B1e z+uRM31DQ9n#@P3XFwfpjU$SWT&3S=QiIB6P>V7i+o-uwsAH*?rOt+RE828urb?L+Y zLJEtfuBNC^uM65*eEl`(qyNz582}bHUzpR}s@B6xC%lR+&iygMefpUAH27mLzmDnJ(W`3+aVMV166|0d z=UgX{p{12S?sgF&doesCN~Q;0ILn3AHHohu=+G>xZpQ>?bTwPJe;At~hgZ|rk&wt* zzyX5T^xN8qoT0$61odFSt_fQSL#nXmzw6JVxE6#@Fs^*s0BJW{ zmPwLtx`4kDNAb7QDjO4{!osmnVSOA@TG^#)R6i?hck^t4Z_Guy1?SgufyZ2a)-Ffx zQnd!weXZ(i264)sG)8-x&*EK8U2`_8hTF9$gogtN=0vWZDw+s(d7WKXiAP=hJ?rEf zS<2ZxJ{(XDiBiXJvwufBLA)zyP1a_51q0{*A4KzclMQN4{QLFG|37S=4BywdP7Eyl zhEo^Nj&!$WCbAe+#u6PVk}q3}r4V~Re7}1nLKh(I&O0MM5&c7^iS7_AoSpUt^=a7m z>pO?Lx8Ig#=0(l1RbIoT$x?Fqsbvjz$1<{lxueE=ZIoU*SMH&hShQB(Ise%U47qTMRW-iERl3p&4($U7dnJvr9SJFsov!K~Sx$m-T8kUFjan zlsNU|;>Q36c#Bew_hh|eh8rWPTZv^}>Mw@8OT71@{7?^s2EF7L93(t{SLP*LmG+Ur z8C;S3?VEes*F}j@T_i4Crqxvszk!$=^&_*S1EYqaW2UCI;cvTjlF{h zEjjh9HDZ^!8gdStJv^tVd~LX>p18R^43%4iv6fr53z?|i~|54&(@{F72kSIQU7jMegCv(Bh zSXCRp*f2X6`ZNGOIXlo9P@@I8Xi5BB#QBBP9*QUi>9#%bD_3J+Jd_F-Lfd zI8Xh(DVab}wd-?3%HEO0m&KXmoec~)S;#To5wvGTINj1yEg~@wO6LjO{oEhC;CdR=M=QN(2`&$$*<`$pyMN^uhCRDeDu?LRg z`bui_hBb_5EipTo2uE?>(rH9ttzNd9`n_Gu? zk4~yT9!*RjLuX^Pu?1f#Iw&2Tit-{IIWoSi2Idc@zOW$U+(0ieBAmarFQOxrJ^(G; zB$~qK#<)$2(3GO|<9-P%%@64`F+&}EZ=kEKtiuZf@2+6O+Y=hIN*ilm-o|VLvG?_+ z(fX5Ul}NS2qJo&6_dKn0`9w)yg{A>HwStP zu>V#}um6h3doi}5D!es>)zrMpcj!C`Os*Kw=%i;?8a672M;0Dte@`5- zDu$l~!RKz$c*GY3U0+}R ziO&MMv^G@K$`}d68tI@|`@cD)^LGrtW!P4@LyRgo0<`OeD{#x_b_6NO({U2N)>1e; zHR{ybDCqqU$tC+M@o!521A@XORFf;ocBO(8{JY(qmZ@oS_VMUj(P<3QslgM1j)Z(M zT(W*J^70wKRbUb=WeTUeySyAu**DZ!k8BUBuv4wmeU@$o{`oG?7_kCN#FS?S^=WnO zl&8_0h%cmlCj4T~kYEEagZ3w>?utIot7wTSkBW_5F)*qJ4`qflJJ;@Glmko%vDJqJsN= z6!|Y7@C63#N*wOI5V+G}*I|>FuxB=>9`2*yg{Q~jlB@5A6qIJ$w?w(5dW(b~TJ}UD z_i<}Ofy`IOb0`8fZ5@oXtm{c+sebgZe>M0g{^*GbCppJGUTdn(wp#qlo?s*-Pen^> z7?vm*j2lD4KDkN+*vY-uOcl)tr&%x)r^9QSEdYh0zu7#d+3pdqFl=i<|@nzv#EH56R(x3(vx2&bU9MD_II2%bMWG+J0po^s7EVUS?|a~ z570q1^V0mVq+B2pm%{W9eR)w>6we4dc)CtkLDx^R;&Ob$4YIVyWZ!xNR%o2rlJ$1R zH46#tOl);4{0!(^$7w5Z)fVTaOg(OeOSmHd@-CDe4RjSZi zI1Hpp?KJ}PmVYcASU)=is5-BIML@Uoj2=vKq#>owGFk2}re#>zVpCjv>|A$RCTTR@x1h1bn9<8zPiLY{_P!i_qsC)HybcU(Gc2?k*=*Z? z*!BC^lwgO!*bCtatHrulzYsBcO%o zs>7D(n^^^g1o?;y`a*ftKY(@$Ihc|j5nRz8`2Ko9{`R@>N9eS5G}~uy!pp-Hc*uaV z7iJR0lj*oKs=bH%)Crmy$BsPWPP;yM5};$hvY$N?B+Ymptfd!y*@CQ;Z2)xJN3Xv6?~^e-s}i5* zIpCd#cI;P2+f(Nb;2Ca*QW0v*E%)8OapP+PnNK!X^tJ_c$XNPZ55l4||N0MBmqE1B zYdsyWJ?vuY7hNT`e~iMUKODX^9(ro5zw%I4|I4g4VUWy~;9i~sVn8$R52S0tdm`c? z;z-$MZem0{fkCRKko>@=QNO|GZk--3zh1(CQ?QVCmp4|oLDxQ$~uKwM# zh%W4rx#f;hvtQg0R!!%rOrb|fT>ZYi zv89ukY7@a5TLguW`->ir|dvrLME`g-4HWxf_bY*+9UJP-&An6-?w}4ZSD*mVa zhrg0D?*u*Z$RZs(k9DYR*V0Zp$zI!EO^JZ3v9mb>z2=q;4ZgttN~t%#Bpsv(!j0SC zMF`gC(#?2DHKD6$`~Am%fuI)SR-ymQk@tW9NAbFum=@mNPo-9cmW-fSu#Pz0Xx_Sg%3ghOOOGX{xw}_hh_)t%ug?)H!P&EBHH|#%pjRezLE0zah&EILAw~x( zYDvHue->L{+6b>f6@%5sV^Ge)AxoIpY3!A#+z~+PVxY65W5~?&vJr7%xg*|)*;vs0 z$(^?;4@AReEU;fBKqhOM4jZ#9m(P%3iCW%qkbvPr3JI<|8U zY0CxWY>efrK^e+Xur~ZSTvGncVCsRoaZwunKYLX66W?F&9v@a%98a(FAxBODJmc`{ zcD1l+R=Cd)ftXq)f6n|so6h$GELtxon1Wkg%Xw4=7dG}SvBXBZXpC_N<)Ve6;pf9R z;nS=QF9I&&SF6V9lZri2RO6$<8!G$J<1n~zgCGwVAK0)AXk7pD4Uo8yRh#yOwQob~0b9G`Edgv}HV@Q%c-f_p5O zlJ*_^2RA@`t%u9Kow_=6(G!D=%b9YA@wW$vVhr%3iSEvr2Ht+_FlXyzPu!X6Wmv^apMyfB$+rZ_SEP&>~64IyLFjm-)FY-j9Vd!`E-mvnx(s zJW``BT;`Mb+uX*z_iGb@+&8^A!3r@N__eUP7wV=9uFKTK0?m;<(0Gs@$R_4CP>Uwc zuPRbS#>htmt#yZjZa{lRmR*a=X&O-r;cE4_?%-s6^v-LIfSz>^G^MSfa4EEZRj3pxBeXwh&Y=5b( z#<81hypP4K3!yGnT~4cGSx}}du9U|Qf1Cw(be@Ywvvxh#HQJPz@fI`O6O*LsQpp-x{W{JPbc5h)iu59J#&%8P5Igj*N)fOS6!J$c}K6~ zVIV!&kwyJe)t}i$?@EqF`OA{m6+P%2CH!+J13FYh4X_9H#2YDvR<%w3R|6-zgs?EvK0218M_h@(o=~A{joZlV z^fe+ZiRP&AQNk7_n4z*zL55JP7xnXe!%H`Q46IE%4wgR^ePKX&T_p4Lb#zkbQo=?_ zmzIa3&jl&#IUl)Pb*y@#9h>626|<`GwodmrlyGLm)SKu~y$6;ftc^z5Rrr3pw};X? z_*1moe1+Y1FT_rQ8MiM;3No&rXpSP`!OWwB-_%S(pM90gLgFW&MR=Ez_k6HpZ3l_s z++gSK0mGIM3RCQzYWj~UrHK=9ppb2ji#GHsdom?33{az7daSdCj9oXtS?IrC9#CM! z?T~)QMvn|mk3m4=iy1d>x)Pj++4?J{)jEDALXFK9%F-vlmHt>({_*J(PS)&3?!?qA zBetl5PE)I}yA}R!D-*%hM}TdhFJk^fo)4kjMPk>+A-aClmTAX{@;Ik7e)sVPp!k^{ zAhwX#k4buFY{|bLa4SHoE9l$BbKmfjSIdq{4f$5|@21(swhl82Rx?Uos6p+rZIE^= zqr-c(%){(Qur|ybXcR%W=YEPgv4B1O8(yza3Li!Wex_W$vzG>)QsrD4c!f&ZMTmTe z0KE^ESMxGDjMM7AJsCVX6qbfGD~e4GzqV8(Gq8Q3e0wzM$qKbSuVHb5+)MFK?F$86 z!$|N3i;&`*Inkkh+fFo#M6>U$m*p+jA%|B7#)Q$2jys>T?tASY6xpM@h|Sl`YfC~3 zJC%INw)U}&%p{qqHcN@GMib3qc0nag&fPP_qb8}7!Lp4u%^_tL;cB6!I^fWdVEtoU zWc`V}rd{|mwu1@~doGYuaS0!OeCG4G-;9X#7|_8z-3>O1>Ha1yukR8EZmxz9ai&Or z6oPqrx@bc8)Nba$AM>+a9u5V87aGe>I88=(gkunPvGEJ3iDGBBRV_{Z0_9r2*+33h z^z$$O)zsVjM+H16YCNBMZpEs4;N6#F?Kjsg*Tb>Sd$SHa$tZF|4HDnG+iT+TPuf&Q$thBr{!)-Y@K~WS>rERc_4dNI0lECi8)wZ-0>Z-U z)A6C6OyVyt>gJMsk;Yr;_f;B0=I1AGY7C|mHl|6_wbF6}Zddo#Xh575w{&dMCG>|A zV;x(#HqNfIcbau&!T0C_8if@sSW$ZePhxov9P|TE5t2X_u-74|AOjEoxW*_Ly;?h? z%)-%W2<13)Ims`C3&BE5-{=jSJsDelv$@6F^_0_@3W34qx@tCTtBe*`F~~6(Q(IKf z%Qhn~xy65S@?XANU#si|^jVR<(NW=B9+mhv_-rs}=giJA`jO@|@Y1SH+G(|lqRCi#Vv?7=xWhMZ_E(}a%tAp7_K z+`#z%Ql1B5+37qjfP}fc5p0=)v~RJXAYr8DA#E$j>H>1l#~D<3}! zP^+E=GS80z8Hg<0JC4eSA)5M%xtfIF-O|u0?N1ZC0Ae{+wJ#JuV-eT?17mOZD6{(B zgDO9BSBnP3+BokKZ2k-OEJa_L6G{z>Tc}2&>e19LOtz|Waru79gj&v(aM>)rn|F_C zktVS-IR2)n6qhvHq)HhqJb3-aWW9{s23yVtYt1>mogPo}&|1;D+gggL>Z2Ga?Zj2iBX&E$rN)${`q=XVYM9+uRUidcTWtH)wgVUnF)7h*3rqKB=!oXkJf ztUI9#>&hKACPy5=gISEdZI|{R3Z-8!87XU5Kl#*j@)y#z4D?Q|B6kFn)k4Ac0-%^e z0Km5WTc`{`fEr^x5vP^nLiD5`~4S2v*J#ZP?mInrJ>ahX`;GlzR+Bk*+}{e-a7O$@X9jS=P1O% zsh&ux&b;45XF?K0nrXJ6^?b$lgZ$)X^b$q98{<(I8YpK!;8#P40#w6Su|+wvaYc1b zH6BIL`s}G`1jH`X3f6WF$O~}H{EKsvdFJ@{XB`Vi-+s7yCa~ygKwwI1*t%<8ZpB^E zT4RQt1;RO;%tQ&f7UzdGA{!_**fN&*8yM9U$CN~N2xEl)h-m^PbBv$7V@ReSm9iMt zTvPE{|KfUBVpV~T;iw`xnLC-j=iYcbHetC)CEMM+9?aGxDJdc zdP1^kq!jN--7AnVE#FXYTlP5T+-Yh8Pe0*#AUOXl3ny%-nU6YC)vdUlQu~-dGh_pN zOYFaV=e`eh*FVd%oS3#9L-qsV0RQrR#cl4hpTwv$U$OOLG?{T#Oh9aY=qf#;KR!Za zV$Cv6jm{dnMK>3UYi@o>GeHRN2b_W_FYe@Yh`W|V5AWGDur(MMC%Ks2T9WY}gkSXf zdJEZ4V9}m;Isni6i*tW_t$oOM!4OUzUSf+Z@S0k(IoGmz;^QZ?*Q_z0pks=eN_|27 z1Fx=n-@4Od!b_u8cWTdo5=+e0HY(&p`JFVUESWa;?Fhnj4Z40?<@oVZ*ZAlfhCSfw zs9D>llru9<-Tr2V(tU5#i3z{zuel#4HWw$J3@eK{wRl72Lr{Xvg8f;t>`)g7VP2mt zYmPTjsFn-arBEF<;nBc>mP{M_x)#q%S>iyKp$NJXlRO&7ZmXFvJ2rgMn7= zVGssz?>3ZPq!5le!PwQ`;1+Up<|7+iRCwpAQ%{kVUH3O5#E?;52R0$M<-d+ zdtbR3F@nC-fuES$RDlFtx!_Q^&^RQ7a?(30`mtl1RECgAwz4;Vn0%S?{K$ik&(FX5 z1+!qDUo><}cN5gcx@1;9@s9r8pRu)MH8j=DLq($MbaAK5AF%}-r|%O+$kVLmeMrd+ zbc;1Lrfj70Q#kc%`Lgt(=g)y;Yp%H&41+lmc*_6gz|0!?nv7kWb^S(vU3Ps%`MP&G z8rNTEVxk2!3)QYUe-W1WM4C=@`j<~t zIpyM_z;R|ae@f?)ioC&Pt^7b5P!iPEHrBpqGxNAiJKm5GtVxh__KKRQw(1_um$=l` zfBHB9)#pT(^TXynq??_$dcSupDf{;QLH=v2b`pnPYRTab?%T7378s-1id9()%7iAF z=m+gq!G+g<2&XDIsuDzuE z)Qx&lD7P0=*Cj9Jr)G7bs)rzD{V>MI8>D9DQoUY|J{=XIWzcWVs#Y6lM6uS_CYM6_ z9YdVil{@7ZK+0*yBs{lMa&TZo6q8aabf$-ExOlmqv{Siqw$%+BPM(}h!@Iz)Xl(Y& zs3mm_RTC;~{gc(;d&J3}Nmw1`WsAl85Ty@TcecUMuZYVA$gZ|oou2gQtgr>>`eV10 zTekr#8O-#>4kmc9;@td2<$%=5TXn!%d#2e%u|%dKa$~=q_JUWv;3L#aoyd3$5%xP} zTPD@gSg^O=kP6sqkLghfi+(4%^smK+C!2PFPtxyrQCI#sH?_eKyA!N$evmx;%UJjX zaM8UN8+~t8#Wpsd3Ok}uPHzyt!62;>%Nv>UTNw*U=|SlhXPqtBv3k=q8mKaI-uR* zp7yn*f?qe(tFy3gIiO0zP8Ef{O!t6q1HCgpe*Rpl<;pb{9xu8coFpBq_lM}bj*3!9 zcxP;k3YOyzAc$jY!EXR7p(psI_STVFs_#O1`I%-glBsj2h2SnR*i-O)+H~H(XRb%@ z=73VW3BKbI)IZ}2kIsF(9`r2(7AZvwUuQ(YKDBKW)hcav(+1*tJ}Gm zx(0!PN^!iSob%Mqpx)FAPOx^|iGSW*R7gr(o_0>sEo#;5b`}+Dm=e8cyjfx%Y4}%3 zA}Ztit_Z}1aumLc-_>^2IeV#$yipAs>#L4*-B|0!r~-4?wFnJY_MKlbX7iL&!kCd| z5%DJW<`E8Xf2 zhM}KngmcLjs73nvpUJgus3rgwf}qmA)R_vP)!>B=g?~t1{-Z@XQ*rgC!m@mq`-hGt zXAiqae;>LvAG(~d6rNqgRLt!)8ksyL1g0p2i)^h!_~YF5VE{?)U48_>yPR$p_i4&$VHRZZkK6{h#)CMfSg-X%~w8 zGrYu{gw1;8h#Wp3(kdU^|}z5SQ(`+7qIJls~A5wb6C|89w4eyjDXMn7Q7e38XfDuESD@}LE!z#CX% zG>p~a_KG%3wjpNDaWDJvc-I^8ewqFI<@Yq+N$}klVdTgP9o0m1FyH)12A`GvR9fp)5QshjRHS=R)G+=0d><-Z^I618dY^ z?7hdBc(?U>WKnTXa?h*$lS?V|{X&&L?w<$pvc63Y@Qj;@7@`Bx6F3VD?}#sPKN!y# zcv7FievA-3Yyzlj!ZNM*I8=kpm%g{X2r{?N5efQ=^&gJ*wu{$0-_jS2~smXBYu$UPq)w`Bk3l(rZ zd^-aBb2RNtOGVPI-EEb>!Wt!Ch`!e9xyrI%{hj&`_QIneh-!}-MHM@dyADCJS27W6 zZoggS=RNzg#1hbrJ$HDIFmdh`yiw4}2AN4OU9$ysJ4pehPL@sePXIu&SiH#V7|1P_ zXyA==%K!2~x{-phno&PCup83jn`Mh0$IGyx#v1?fS=Q2cYdD;3^Ym}dW2!ADu<78Kw;OiR=vy&s*NMfa@}a7Cl%}MfO#P zV(k|nhri9GxorSfSDovHJ*#qE`VuOf92ORqxObry++_1BCSatJKt)~zg3q1U^WJp@ zi)3e4yF?Tnhym<^X#5I0%>8eV>6qMzTdM+!i08+ht+X(i1m#Qh|MgE+rnnOP&1cx) z)JRwFEso>oe&N1vR? z`j~Ml8yHF8wfSa0xU_mvQiAIM z`WeOIEwrAk3$cqdi5IYWg^hBRlbiiQQh<<~_)|h!^@XGPGo|<>(4twSjXeeD$$xzH zS&=J6H@jtui^E#tq9!bJL;g0yDb9P|Eg{X~CVNG4%jK5VfwWe1RT9|D-MQIs$x1P{ z7!SPMW~f`FJ6XXh*KvqHmEQV@WqZ>oi1nJvM@_FS zuatuu6`UEXE}5=i@bI`wgO&2FyYJDL9?wV9S|3tzl#+rfCRiVHRzB3WcI@0cSf%%< z#sggXEuxBnd*J2lG{8*Y2d=1!(F zv)69NEpG$Sd-5am&q0HcVA`nbKXx| zeiZvBgVLnllVtwg*CsU<_a-szl1_4|99PjMf1#?%#ERNfo`A^&W2Uf#H|Ybjvn$e* zeV(7KLfn$jwL5z2DA8dwI`)N@9(*4xKI7Zhzgu2RlOBs7F7VBk+j;A;*KgM2+UvhJ zN`>R!RF7Uw7OkzZn6Gktwv^=BRF(ksVsa!Q>7X7%60fn={1ro(-2X zNk!ka_P*4vX5edY=RV-`eyrHl&2=dW1iLLv6R8Lej_r%hy|E7I^$-3C-3ifbRj!=q z)>Tut5EkEl1@`>n_Jevq1F`y8bri2HipD6;_(2~t)yy9y;=$p&BpjW%S-v&NB znuSS1a+yW_(5T+#`Zt$GWTR<@v6kvzO$oF6BHcdG+G~*v5d+ zm^4)Vh(xKLw#?{b5agc1}pEsqCB00X?n@VE;DTiXXz)!UDoHgZP?$ zeBtXFD23ohe`9_jeMz*m4UvErIniQlr{1q`A$P%YxsjvHhfyZk8Mw z92<8jl3S|m*`&+YJEjN4n;6Uq3gLTPOYQp`vK%1WuEm2aHWn>7hWOlefG1p|_rA;h z3GeJ>Ou2SoqvQI3Ro=gyjArIgLwR9ohw*`!21lZQYo zxLe#mFj{Vm=pwiGOyp4Fyto@?VS%oZzzF~J;iPG^^l3hU^YS4^Tx7vV=K0{VOOaSW8q*r8@I2rFIrkp zP~HQvWIn2|zI?f|-I%7&8u~{S@`gfXZDRGVUNvH5K!Q3Z?WD?GqX> zv9WD9jqTM$i4q>Mle%>)h$FA*aCJibyoefd$4A)HH!YVMo=TYmCY$4tV%hyJ$n_j0{q*kk6*%+R7_j;{^;D1PO8(QXg`%*(!(3r;J| z+Rtjg z9QCAbPOUNpfR=3`tve`d3rinu{c-;OyQUQ}opz9Y zYks4N&Dc25zt9KT7UXKmM?&<24G6@N!v%w8M`M6Rz!D3nq!hPHwaKCHZ2R;QxxXC5 zbsJDx0H=A!@pMOX7Kc*c&vf43v8pj7cQ3|Xhi=Fkg@dQEl`lofXH&RQ_7s7A zjHgrPeGHD`p`qw`&_c#QSv&TRMfu3W48Ui`*RZK9Ga@T<*m8q1kVHsc4Rx&AK2ehs zAgq}T-7t?AmYZK5hncPgePZ;#V1FbuBwLN1=}Ub|bv$~;@-kg(5>k`k#?2TWcqYQ$ zeduScXL{mPb@}tfsjvi0TPSh@4s^EG3Z}cym9Oh$hW9em5}Jl=sS=Q*DI>cyh|^9x zFsJ*#6e;ik0?m86Ev2RpUimAKep2&sTuoH4KBG(j-E~Y%?ll#UH)jk|W%iylsd`F# zrHbH?luN}<9CT*bU&Sp9Uy9$jZqP`;c?+PkrWW@}+Q`>YDx>FTcPbVT>_A(58)d%s ze^BQ-sc(Go@y%(!=OSqUe0b>LdC=s&MeRuG@CpD_$ApJ(-0M|(Bc{9$K(I)SdvqdqP}v>-%zw2!A={Q1zVq{- z!smb8S*gaDyw8azf_<3OYLta7fw(5r0A5ghZVXqHm?*xfCM;>J{RD5n3xC9ml8{FO zQ81rf=|2u>U*zSLJas$X^G6D$`E)zvEF%_qrmU=NrHY9k!?9{Qw`0bbG$<6!S%R1K zc9^^lh3$Qy>#%UnS>{ms$rC1AF7kQ#*^x9$8lFt?P}`UFD!T~Sr96%Q^2cRsl59AU z#q=Zx^w^H0_~C~1mF=B-!fusnR@5!KT)ydXzp-*G80*`o863su7cON5Oqy&7 zjblK)9-^cB*A_b!kC>aXj18Ve$P#G%B z;HT`Jw8sSrC8)GZ>Wy_dOG|L<4B2<=uKk7kI#x`GUeSz#f(}ZFRAlc;PXa>ah4E9} zRwvc`ci|9_3F1rAA)r)5C0&R`%^U*4U*rK*8nzp+-o2ZY9aG7J+vU0oyGN`2>puu8 zr}OebW_i)->4rmbpkwb`DlgLxl3*w)``xz8jU+5RG2!f3Mb)*b)%8Fo2%+DPA;IgdZ z^KEb1-Y@LZeUZOm(E*l{JIzIGA&E}b!DdndI1hvJ!D8rbV{=4Jw)32$Qq$jcs2Mv#59u!%g z4vTy5RBfny^exUlkqoluw!Nwsmdc+Ei{|Ix@bS%Tjul{%jIM7F3_z`~)}ovnvF2}Vqp)&b@Pi-xZle50qf*=5JYA$Jz%C}frfx_(n z+p`cyE-LE_HzRiiWrRmiE?<3dU`|{Ap#7dlhKop`@@}=Tml}d89GLoFWbQSV~nDQfVsO-~Rb!mJsbV}qY)6nj*kh=PobC;yh^v#G#UjsskgJ!_m zpTA7xk9&mX!p+(?JMyh@gy^Lqkymlq&8y$lX*46ycX%P?P0EjRK&XJQcu47!|X0AVG9pfP%JjuoiW)8V$*BMr*FwzHWbM%nq5htY-A)WNZ!csA4@Ye zR|%fEdM|X_>mJly54wT(+=?;J)%?+XKV-JhS2h%#Q$2|u<5IlNgq|QGzmJ@^dodlN zlpYqx6eaS|vxT(Zi+N#Q+BL@MQU-U+{Z+ki;oQu4lqzMQWjRsy$LZLOflSJzG916d z=H*m#paoa%RE6#Hd1`OvB~T>k{hl{<$qSpyK3kh=mg;St;G$+i`YoKV&Z++3Abw=B z+Cv~kBn>o|M$S6_x954NOJm&mQ#Mypo@I%?aLuO>U%!F~t+nG!c zwLs340|~B5apIQ(h4W-AbQxf?YwE?FiOO>1MXq7EVqLxXEl(TAKcnvx{v8S(8J*`w-+Do>Lhu2OFDzFXckTD5qEvA?|&n^7sy(#e&l8 z30j^fh^UBm6I-v9V7UFZy6zcGdsc$`9wPmzL614A7w@<74?Bjt3?Xk5jDs~%0u1N8 zu~*7P6+4~VNp!umdOyPY%>bj3wi*rQh$uH&(3dTv>a(yV^5JKft+8LJOjLjToi6>S zZ9ufr#ubMvQCpxiM`v2gofyEtl7(D2Q|Q`Cjq&?8mbMKPAjZ9#lF{J7&#md(D*A8T zuRD9C)i?#9lF_+)KU*m00`WhTkLgsI$;_>JnI+J zXPTegp}-)4*z;EgeLvIw9+)poE4z19rbpZibier`4((?_7Zm}9VNdykl1t@2i`;=t z^Q-G@fh>f7A$HkTSh9X>b{<&}R*(JFdROvLnAo0v=BuMI5hl}-gEznO0s<~M6V^OI zWSD4HKK|{LJq1%2IPl+|eejCiNrV9lu?Fp3?wo!dq6w2?u+v()yC-6#w5_6M=L?m# zr~5)u(u@wD*tl{b^Y0~Nw}RCEuzRodmIKs#Z2F7zl7dA;Y~RJ|w19_8-ILbUPVod2 z4~QobVd@0jKI`?E&H>q!&{w;lE_~?nj1R^DjI$qF{icnWC<#(i#Te?Xxuf?#xSAaB zU^3rk{e>PmY7Wby4^TQ6IR2c6SxvNZ>3E)UTxy9$|!Uz3kM=jz>j>f5LtE=onCHWQ8j(@1R_! zjQmc*B=9JbD@oKExLZzH$nX8ylk%$i*mF5fgJ>~h9-;qPeTGTy2`)}!&4ycF?IsLQ^Kbn; zpc<)Lv}ZcCs1px62>Ojo`&Mb~yVE`LzTiC>EmfMdI=MZ;`5pVp9%W0NX@vUYL+5|c z0Pir430#i%aB18RLk=!?Q-%5&b^aOK6aUoMJC#rASn%4cr4Z-_MJ7zg|3hQs{Tc9I2X)ByrGhyXHUi zyT}FJDLfm*qXq!sD-f?XHtQv!2jKH>o8LP|6`z zNHjX{w^dG|E5?Dq;bwK76lod}?-tX+{n*CzHMqFnV$H&7brJ!myR{SIPW|_7@1Dr^ zMWp=)Gic@p$pNy&S2Z4lk0LCF@*X~<9qa*qkXTl_5hy#NY;LuYg~w7VJN=9^p*u9V5rnv|u0Qur z*wvwk!z&ynac6~M5!o}e0`WrIHDVzwPlsz@8w$KexFSr_bhgOKyhSu}w7g~5D+9Py zLN7^xJG4180RgR3+#u01=@~6pwpQxj_cHuR`{Gl>Lq>+6S!f!m`XE4TtC?CsZ;%{I ztnAKDrBqM@pn&A+xHxj+=61xrd-hrKUpB_uFQjgZ{r<)LxsDjZ!|L^WeM5B0UCSg@ zi2|E9p6V()KDiu}k@|dX!8U2ZDS24cx2*sbWv1@~wd(0xaKY+d@AK^#iEVPSuYL07zng5Qe?U_<;D2sR95B6I8#|$QX=~K?u(hpw zXFd}(NwwB0YC)(aPg@oDU($s(_4NU1yVdI8!2}$~g9C~KHo0(~p?c3xy8jrVp}Myd z{R(^DgS{eMaJvbszs*-%c6@+d=miv<2**Sg^Q(f2+h72?%2YYl#@_U21v@qbP28Y) z=56j|Jh@YWbQ^ypWn)6L`Z0oUc4BRkqd*BshvDs>AYbtmrXRE%=Cc-3QaSJ_qykg7 zdCkS`rbgP_fdivarum$;58Qm0FqE>4AR_hh7t-&dZ98TgPG>V{@u_d7(dUsau+E%) zDV~I*QkaojxNE$O8PG|pyC?H(clDgqTbg~QRiSB&KUzY?Vejai&tgR+#P3Ca<(-Np ztmAeXAY1wrA%e6`Vn8Nnuj)(baya|Dk$QK)W63Uw_Oo_Iqnstye)4GCVU@%CwYdE|Yn_TbrDhYy8)4+C7fNY%lo9~EGW zRw`QjMUc4e*B_TEJ@PQQ9n0*-Tu-<@jJuw@9kM3Xvm&GG@aHcp?@&eB(TRW@jO!+W z_TY+9K+=!-Wb~!@my3EpxH;U_{_YE-<{o0c+SY%23^sqqMh*nFq#?Zol#yT$k1Z_3 zV5feECM*L@Qx(TMyAE#lh)%Ctb)(p)n9;e^mw8Aq@8#)6#2wGg_CMZf7UJH`JKqlR zI#?$*L$EfWfM9Rz^bVf zh=htWkREZKYOg?Y|91GgUKi+y`92=9t4*8~T)ldZ92G121zzRkPsL7ly`58;qwW)w zrRIymXG0HMTqIvoebr+)ntGaPh}sw^NV4*MjGpHKvA=7?^z4lTHjZ7>v&^9o@&y9` z-~<-2-CX8Uoc#;FHS9XiGeJLVonb>VJzhzzV`8JRImBa~Rn)7=yJSKVUmltp`jw8l=yxcN7rOoj45CgKRb8Qgi0q3!67X zWoLzxlR(R}%Zl86bz#@y!DgtRRLX#&X(e!q*?8m^MB=u)F$5y{e!qz21mONJ_Zk(b)|@*JI-DhQ@)UK(Ngv@VM3z(JzvF;wOFDYV zvXm>C+i9S{#?rZGzl)VVh8y-o7$-uf%NAuC4aX-dzHG(N;1Hdy{3xc)mB0X`G~UD{ z7%)m$_#5G0L{5f%4(mEmj5l1_8Cyb_HHXmZNCe^|!T7L|W9gaqSUVQn0rw-{=i6VK zKN*EWcpq~R$^pNDt%ucPZ&4*kK4XvVx4o%&<^^f$1 zOTy#tGBN^a-Rbw5XxtK-T)sqyKo=9z}JDLdKp`3DXLM zH|VOF!l!XP=9#INhV&%hIB2Ogcs&tK`M#rhVb<8)((P`4^~HYifC_FKFvPVl5f@pa zqs{9@3$<%C&r0r7C-0B*Fe1vGmsdnShcTqmQ0lagL~5}P?NA1%{x`eh@rK}llxxvF zc!%q0i+*b3#icF&Hj4sZM=`a!iC2Hro^>zB>xBCYe`=4ZlByQeMKI$dH#*l;Y zKPGkp-SvFwgYn#lnUV~%H61#3(c>Zz`a_!B6g4dSf0~a8CX-t^{1iQ<|La}I|DidM z+|$vzDxdDK`SD=flS1I{Fl3%*20t3C*CZL*Wo|JFTwbj|2lp;TD5sVvj1OrWaanel zSzOE(2|)07;M3C%<2Rgy`S3vT@eA8e)_UF@%q|B|Q`OWYwb;yE^`249P zYDycUVFPF5HZC6}nEY7~+Z{&kXztVfw9FYB;^G1OMPYhNpmu7rYyS!jol_LG2`Y#4 zE+7SxZZm8kR?jpxXb0%U{qtNykOGqVw=+G-AyIZ`-S&HVpD3j63!0cE4<# zH9;#1wAL*uN@si$Y^(k57TEvPZF(_3Ay(VIU8&`Qv^rM%EGtzne1VgF@tRZU)C=3+ zICXU{0p(Y6;0GFMM|Th$LgzxatTwIJfraxC6#v1LKz&_&#bNyurIVWes{FP0XlQk# zi6L7|D_rtQ)je8YlYo09Q*zDP?1VY1^S)_#)LQYdnpd^m{X!3}(iYeqMv%yoSw%vp z1|S6VAal3gLv%Qcf1BsMeN%{nC057Q?XquwCBu$|&!jl{J_;>ZF(^L2*4S_P&9eJW zd0FcH^us-tP*xLLGyAgc{qtYe?_#QP#!;psutHFv2@a>l!Vt!|pfT?b0h=X9$ZkT(z1M!(s(Y^7E@P}Nju_Qoq!p$UP*TX&o% zlw)y?zAPRsk+``7VEG>i4~14Yc{2=pnn-FmxVlxfJl5@+WmDp{vSw?$t}AUC<9$W} zfAjI;49G8tC0mN+TWFU#&$-%o$_3SSZ{~f&<0`FWN(^apl=nq+ve6dRn`02aN~J?c zi4Jr5{qAUu^9}D`>aV@HARBglg+IQC1PwI8hbb?wcC6D6%uNh$Z^WGu|8r-48NKo~ zJcbU}aHN#%U@n|?Eu=a&Nall8c0z-v2$h*Fr}K66ewY+QyFn$MjB@uxB+L~HDDyJt zCp$rrU_E4R&r*Gn2&*oL`Az3w@3@!SDpiF(rf3Z~fIOv~Y-BzBZ}E*!?_ZrOTznf} z_;^JU~YxPcnoR=^cHcpu7( z#1s|`hsxLdw+B_YS~^3MeO=br@Jvdl;`{){Aj(Lo_!}3!p~dv=)Q!C%o{BddPK_{h z3q0*aAmf&E2+1Ienf}m|Z%Wi5+%Yn3nT`i`qMe9gN+-=75dS{473i$RpEFLY zsmU^CaIYQ2!Y@3$@isPSn*Toz-3W6n;#X+*7t(39f$=^|^O$X2@(Lb3$Jqo`)TJsB zYW*!N2h|c$iIs6XXghS|E)w`iGZC3^LL@J^g#D?<=YQ9_+@=UK8Y#QC;y!x+*Kcml z=5g8;XM?@g<`KLYjrB?`In-} z0&C_R0LagI3nqW>WorA>;dAB#QG z+Hdr7Un}FZd4Q3-NkeW8mcG7|UUsQ`Qnz>v4!`k_vG$RZFQqvXN7#j_#j|+-p`vOT z%YGq@k6nz+d$4GPKb?=KZLubFPR%_vnk#%JAj-pg>Wj}=)C4eNf{T+WfO-6{ui^P# zvx(+vS~;^lH_o*mLL-qsV+8(-NEbje;n%Xs5l2;>{TxB_K;ffVVYj|TYib`}87(${ zwZB+Y6j*doZ?$(l9@)0+Ak!*WobQnxuqhBGF>i*=6_xoJ^_lH%sdBy~KEKc!v&xSL zX^&|PprQf1>9^*0XFtVVsjY7npWAU4*^646hh7Iyt%_1O9N8B`&i^Q)IV$M!ji72k zuX=7pjvKSIh5c>A*Ty|jRSorOO~?po7`=}chWHYz>Y<(?gqg1Xc*RZL$TqBnI<^U1 zvcM`xK$7Cxp7&vTOcH z-#!u?a**d|nQSpMQJQ+?``n}VJ`82$7V_Dt-q$?zHk4<@(9ehb$%|qu<+&)G-(C32 z<<_^o(%ux7r>)`}4?Fm@nn=rS)5pH7hD@n$T!m3)={#lnvL7QldR?m1h<$AE^0Hgk z*)pTdhqu%C&9CmSnM5yCxEDp0=98%tWt#&4}CFt0=~H~ul#vFd?UJbxW*kNY*^Ux9GibW?JXlU|j4M#4#`Jh4N@%h}BZcNs9x_>(-6ERZsM3 z#N**+zyb0FxLE@4++s>Y`+1n&TJ(ztXRWn8QV^1dXd!w0^Y6@hmF*|8h=)vl6jOf0 zN*qd!*=_KRvjLBV^IAfAEP5jD_rj@)%*e)^d`Jb(a~DFa+;B)COs50TuZG#9teLK< z{k28sioQ@26D$*R)e&_%N05slQsF&&u$^rB$37yUj~k08MbSE%pg?p6NbIkiPzC6QwnwSw zz_x~h8gYCjB4p3K2SK&%d*g<}bRfr4cMs2#5n8*17lDDcB1^I!jtrMTCkR@K9>eZd z+uucuEhSW&2t5OWLqBntOWpy9H8A|8f;j$6k~X>3YvWtF>Y1rOTi@9lO%&XYeg#tV ziuW_>g=hDQhFa@Sl{=amJR?ui4fDsNqfUWQY-a6NEd1;esiJZ~-o?_S{H5IzEm3br zvr6Q}m;nqd(BH)WJhirDeq~%K;CvoTBT?!^r_8}$k;EIMwZ)CX|MvVo@N8g0h!B5p zAuuZis#np-mCWj|L?YILIav_LkbE%PTRkp9xRzl)U+3nQy8V*L5~~H9Jf-+3?x{?0sH{X! zre>JE(jDhN9S$u3CQ%va4nKsYspUbbbSFz=*T&jR_&!e~bg3pd!+SS2= zL@JFIT+Tt?0E^52mQo7F9a5E-0zF}jCcq&*I>dwZaxIQ`zmd{P1_h6}!En76J#u_; znGW?hgsRJ~Nb#sy1zzRy!PrLHvA5<#Da9)^wzzatQap`VD4lrq| z;*#Qvnu5l%<36{fRTMPlFd+$#1yP$?%H)QoiEmixzqn@B?bCkp(D}gmdrJ~w+o9Hm zc*XH(k{!T?xxMYIAw8OsUh9~&&2?K6CL~#81?UnCebnif@!xvKYI2Ur{IL_z2XNqH zpzE%l>EOKVqYkKzHuWU5x=ZcfqH zY>?@t9xM9PwkirB502luaJ5n*t6_hwqB>R*vh*u{C~ zoIyF=W>SEO-T)fmWc>DpUObk%G$<_I-jNGUf$g7BRJ=D4;H62T-NE2*&F1Un--_w| z<5O(U@8q|xE|c#ld)$7T>U0}U$)K&I>{S7gY!;Gi95+%%%038o#WrvxOHoKG3~`btDyA zFNGbNnIfPJ9hmQWOpTs*%v=%Na0FHpA^I~$+C;AV| zJnRaYqj0QV0oMU7c6Dp1GkHC)LRHp%##_ok3nk4DEEG)wSr64}8%ylqpt!~hY5jEl zlRbq2n?@#^@MANRtk5M2NJFG08Uz|WbzCz%K;vAL8F=KPf++ZGO!o0FUB{HKJ#@F0 z`MW!gvyV_2tJn=wlztdCGA-uw`5*O`+Hx_^iydBLCvzLKw)?ehm^&a7Ot{`@RF~RYP{240dvI$~CVGql)sO zti|SCoi$+FSp-}!DV|g%vkvx%hpn)iIcBnQ#urO6H7l94FIF6$pZwgJa*t}6wQQ-J zVfg4ThAHJY+0?bw?*4j`3++ z(lls=vIb?O=X%3u@Ltoo(Bvz;e7&!aRnP3q z%l6@D=z;aI#kpUpr{krQYv*~sew2iyP>4oGt!{=yfbp`UHS)IHxx?1J{2zPeW$H(h zqE7ZWjoYGKc08fnbZ69cEn82|D4_yI-zmVvXFfghsgd;dUrE_WOlkq}jH7=prrFd6 z*zMrW*=8%0aGvUX1}{O;p+5_ymtZE9Nn15=ksxDhu%bqJiM7Zq;*KclP6#-Jy^|hp zKao7S>nT#vCzJ+wUHDpHL;eUYFcM&L{#ua%gfbF~*Tu!E;wiU{7&UK#C+TpWsys~M zOJTliG@ zjXend^(EsQ4kfn7G3CO4FGpEyHtO<&KcszWon$PJRBYe>SDIBWV~%kSHQO#?uY z^(W>#)A?ID!#3LsX5%7x7$Foc5Gs@&D-=i zKawZmlBuC!TOF=vax-0hQ!^!8((Yo1b#4BiWoM3)kyfotV}eF0?oWO~hjD3L|Cldq=I1O4fzbmc=X zB5a$3Zk-pCzdk?j;>+)X$1WmA2!*_iPE)U26*Dhh=$(yvwBkNAkxLGRThX0%GhVhj zcPs{)Q)G-=Nu_oM7B_fr0zR=O^(h`jU!)WR_gO|o{S$u@;V@$abh%@LNjEZpyqlpg zvW0G&EE^rl)51D&mI+e-_FUIaaeSW@6TB54wAzv*W@G6LXV7Qi#F7<8xmOuN8@E|U zCMfe1R*c_PG(_7eWwU={qoqBamyM?45C-%1w=*stHvvmaIv#7D$P<8Oruqw^Its*wL>S>Do zkUa&fkDxs0x>WS9VnIgN`b54`wQ}%Q0^MUL`PBAWK~@XcW*5k6fPzdMVNytLrw|oDh2++GML}Yq)lXHz+Kq&Ry)mu7MP8Z z*{$mZMOS(+{hG_%yN%`$o^F7Ryc}|Evdygr_FuEBG@9x&ftKgw3U(2I@@Twwdn_Oo zCK~y*yGf1^*n@xPP9`FpgqEP`H;_6lfwtoE%q;t$c}8d{;#BM^qkErh@U_MRZy~Xf zovt0O?{UkI^bIc>A0WmH4~kR!RB)<1V0#UT|yQhpgs7T+m(S zxS8)00&U$xz&1!kp&v1Io0){$uZ^~}Chc8sd-HLrh4SJH%p$II5)dJuLB&)Y%R8TQ zC z)EWIyxH!NsOlvda`A+j4R#eyMPZ|hPu_hfcxk*Z|24EH0;a>u3?_ayzUGziSH2uW& ztan&bJi?hoa}y?=jq`t?i>%3AUET)FkPT3j?@Mr~7SC{VHndCYUvo*GTY2pMS*uPh zS!RA?J}X`TI<1FGjF^=;{X@rt{;lO8BKD+UKHv=W<)MGX#DmuA-lMF|{VFE4u%D9L zwkBS8W={kQ1lLY zmF+lA>Ss<(IRINMFRLv&@cf_TW$Q%_vc?UKP!k`w=9~*N61_l7I^p`Cbt@00$U{n{E02X;Gd7|-EaK)!r;687nO?D zcMa5#wr{RD%f}oVv>p=*QA-6GG%P+K)Mq5vymF`UBQv4T%(L3BoI@u=LSF5r!rP_b z!HKHd2%g#OMMkkw^IG>*%EyBR2tA zSFrcr(Z%Ggg#g`2%Y|X$xDR~}^T6m;xLQ#&Lk|$5z%9^{AM3=>N7prx0`DCcJ;fE- zjj8A7)*{+GS3;jzs)K2S zSxPQo0IP(^S_l{c`AvOm)IA`eosxg8SJ_7q8Xy zL3e!qhOV=(^rm5hV3ssR+m*6?sHe~#al$Mi;q0!}6Qs@iz}|8GFrg)TmGYRwF;-By zpRBN*OdXhOz!bmGd%Gwdl+_upcvq<2U!G5K@&2Q#+vsyc#R?t+meYP3lVrywK)UM0 z_uv+bZz#fI%vwaJCyl2@VhY?s*!gZuD2w~SjCvQ#&D;QpUWWfXOJ77(0DtONOfSp> zOTD`9eBHvfnn}iK)iv`WU~F4@{NT~dT`#7&J!U1mMRA)`D+KYyejw z8pM9co_x3QH0IBPmQc}SxWDV&F7oGvli&WSkFUC~VW<4Ea~28E!Te{7s_o2z$h*|u z)rkup$B&FeU2;ruXHQMcW7h3WtF1VjQ z8>iwr3J-qMIAE_vD`aMh)K#{MYQyt~*?@*>OrQ^;UT@=A=Sy*Y%Z zj9s}lXBYDkAZR(a!hIxjY~@Wn#@>FPo6G(aW)1o9^icovX|aX2e$efyD;?mX(1V7U zR_u0G*P}oW58JuAgeA_>Zn0;`f$uZsw-(&(tGH>G&9W#7uFM4y*t4Hd3k>r`)4|#% zs>0SJomkXz#^7>1F*!VP!F;u}M+y=qK(J(0zs@NZcPDC^{{7=>(ERk`p)8{{VvX4}z4u$dy@H6xDk=|e(20}az~zGv(SNo?*c z7U1liEINHkB|D0K2EF}+^hAsiJWmuPN-BvFrbL&=?uDL)-5RVtiT04{<{3U*Jer(# zzgH*lIn-tqL68oxGcoW7Qoyk02@)OddP<|N9{SDMj8`SmBjBl3E`BV7;8Y6`JEkiY z8x}iFj5Jq8lCX&N_c_{$WdKxr~hkVUYuD!g%l_@O6k^{86Y%-Y~Z+{K?S5EIpK6Omu={}`C` zK;#9c@0e8dvc)>afBFEZ0d0DB_B*3!4t_abmZ7#@}#Y3u^(}3B|D9o} z6+7FHy=lRB+=@6+kI~X|+|<^tF_RxpmQ6TowIgh{ zq~x`@3u0AG3d?SkpWg@);9S?_ykH_r4xlWTDtt1K;;4mQK2eSofazXN4)faYcVjTy zYqKLnEB~1JR9#h37OW!tn<2>J+3JQ`lI2@qEN9mMc!}3;aHzXv{2=H#C^|C_TpiB* zsp{wX;0gm9t6}=>$@S_D{~+q}L%iy*X{UoH_s)eWb^)WfSi{97&1OUDF~V-cXvLxJ<~?zeC$Qf%v)&!iT5ehxfjqCrgOhj`^Uj(sbol8TD@2 zgjQ=63%0>*n!+DkKsIDji+9q;QZRL|O^>lzvW}qX@q`A&`b)lIaZvac;ZuHSK^D>= zD@8oJ3Ts?R+(6r9G{BKX-l9f|X=ChhHal^mm6f%r<`@e0FIcV%p23(aH#wN`q?m$4 zDX4xV6JSV3{+Vr1`H?)%Akts6-@>k z>4}aZ_?+(V4Q6t?qUT~rTEC8ro(^yoXD$PGtDkB?f9E!k^!7MN&f6uxrPvyUnQWp= z0#eT2hC*B|hxX-fwhs0`>(koW`k>>0Cg*F$?5o+?EkgH#S%aDLA4tXY>-GJ2M2Zd5 zZ~ixf)q4Ei5{!%-5h@n``q636)Lfqna(i9rHVciatyp!S^w7PImB18O9;K=V#dKzi zPK<7~s_S2|=WA;%`g7zNbqd-*a}s5!BR{-bBlMPFW1-gKkz*a{mvNSK7vMlV~Jhb(5Ut&=P= zux0E&tjIB+GKfJHLY zY>HnKpy0Etdp%dwjLqpq`2w-$_zO7|5Zsu0_=Nc}w9XDr_GA9y+ zQHO}DkDKM#9XeMGwkQ)+FC*>3c!*4i3&1Pf43Ph8nXNLyr+_n*pOriNjlP!N{|s@( zN~ChB=KEiRWaqQXIN3gk&{lR|KUp zabcL(9b6%n`*p}r{`{JO*F4;;kiQ`Z|r#?#Z%3avK_K|F7lq~kg zaub_l;FgnXGs)2e9xaN)3FL&$;L!#96C7*xBWJ}ypI?;wslCBJseg3(^dEm6(rmWj z*4-YtpuKC#P82TL3pWdR4@Jo~Y?IBO zhXT`Q{#N+Cm(W?u<9+u|>JbE%Q^mz$Kd&Wr;seN2gv)&}fxf?{u+O17-LTRz^^m11 z%P6F?Bh^_-ePK5wHsutdHcoy|oy&*>Yy@}kj!nYkGiNl}Dq~sND z9Xu28Zmq??FCs46_s*$L1M>mjhWvkg3ewVD*|o#1jO1(&_+G>M`{o|e)T(7zqpBzs zOiq~IUc2NG{IT!}Q`9xqEg)FAji_}?eX)85E^OoV@RwW?)80DWwnNBlTH@0(IB&t= zGJ|sTTkExs0F6^tAB%-!I^F5-lnZy!Ya>}xmKe9X4X^hLAs8?>ZZ|?d?doJ&-Qp(B z#DGhDVF2vw=4*Hfj)ikE3!)i~7jUr*R|YJt!!MX&KPW|C_x*1~Gt*sU^7F%lSX~2} zHfHpP@N!6pxb`R97*F_Z_`A5uU>@d^*N^2}7Qpoj1dpqzPl*Q~k6e#nH6E5YGvA7{ z%}j9_$`|s$-0Lv}O2$lb2Q-mFznA-#a8x?iHFtKZj^Rn-T^%p|`yt$P!1zbSN!onIX;MPHqrz`81h5&XFPy+Fo23)${=9+!K25a~q_2zRi)drCN- z+Ng*uHup}SN4Gqq--PnzU0IGmI`L{|{FHiTUKA&nbd2hm-vdfnWsm096;q~(@0--s zI$7hQxIR)d(#L!dCD!fJ7gVt1SzC>(Bl?C4w+QFfbjHYt@zVC3IS=UiHGkHUTQyM1 zq^;QR3HPe91kNEYV~Q5Uq_e__7oC$#>kh!~8o!hM`8hF>61psLfA+6D^gL#s~f zy;eEpO`z#f<|rn3FJC+zmsn8NOg_i>0hvhtv6MPk?6aOE%daTcmof~|`n%qM@XA?J zY~G`nHZF-Oh5-V-#CWfhV;4b9LkMKC6KQc zO&s^SCZ0~3*DN-W0gcu4>5noNX6feYaJxa08(Z=Np_arE z5S%7-mR$&DSrx&qvqWJyfGo^Tjzye*_Q1u(Fo4h#Z9M3=73T5gy-m>fgHPp_-45%< zeXq~tuF0uuOqNBx(udD z?a9b=zOAtw_v)mazf?I`t0I^vc#HnJ&aWy_HRN}s4mY*ksjk=iEfl!bkn7H?I~YD~XgrfwSJF$wTP>7bgyw- zaz@Fj4}RLeW6kG`<6JeD7d(;n^Ljxa=ALP6p2Xb@(GKaHncsTlmJtBiRh*72e11?cp0hVk6(&2620t==FbtCYw+v^pm5 z%Bfb_1?>)g8&26fNO1$1kR?u09GtRmA`yq)m(Gduu^E|3UXxrJ=l4)y)L0GM_lf+? zPf|*)!5sdk$YA-F2aOYg3-KepQ@ssSyif0`?=|JA0F*o#G_3*%&c0{YhzlB_D8Mmq<-N%+($eH!T7JFw;k_|STPAF2j+ z6?(a}Q&!ucp2|ZW9|Pz4#sUIQ@? z3%j%GTwNQgoKzN{0`OnQGlJQQS_QN2vxs}@(Dock`%8bjqfh!caMg{Y1Uj9JF6}1a zoA8J)H@XgBkc>i4Ht5HhnzR2HcBWJOc#B<;5{j9&#Fc&<+Wbj;r zP|$BoVa7QVzyqng+MAErw?5r=pm=Ga;sjW_pD)0aHOrSb+MI> z)q4O&a8B%G=Jrp=ZN5N?XR5TGbnf!d~=k|8RFYIio#`a(4WEq$-x-f8hWqjGAuyJeais$IcHg7O&V2@zB)r{Q+bVBk7Mw!;k;hcoI0uODkn$vwyPIn!*BNloP2Tky~3Fw(t*Qh z+nCt;bPN&BuECOMn@UR7v@$T#xY(pYm=Uy~5epCSaKTMUVRSa#JiJfx{OIaY^g2n(&+rVae{W78BJeK1SN zel8Q_sYI9_QybBfoN2P|pv;BTj^xek>2}RlqPh>NPK-9o-IqRY8W$?C-ouX(q9n9+ z*k)SC)M_i~j*stOY%?3LCN-(q89^~mx`0TyBJ+;A!Wn9ii=Y?|9#V47*RSU`8iJir?3BfFzSg~Ml z+hT?HEwyskb+nalzN^#Uy-!1nRg2~_B-co26lk;hFR>6P=o6M?TLh7C2I!e>!M+`J&9b@&4Ga1*OHG=m!+{2cc?b87MKVh9Fb=A76l-_ab?KNh4Jh zD@iwfjdadshwZW@mkDPE8j&-9nZGi0AoEtoJl{uUzPdym~ z!aF+o-`rfzV16*Xm^N91<4(?7JAE3sdF$QG!$5@n#Z|62TQ$5{FICVeT~#nP?|8Ug zReXq#?ciA7jr8ovv*s&lOs`hYQBCV0Tt=s^E)bQ3feR5-={rhJ9loK^n#<3IqW6hR zCdJ8;*$#V6ttd^pfy(8q+%pSP<66(!4J{+IAPilgadYL5Y4b=lH<6!oEnA%$cLYPa zPnA2hN{-uya~7lzxo^CGsAADXgt-?Yj?GFY!&K9T+xoN@oj!U$2+J{1TFhueWwF=8 z9(_-NRc1Sd`Slvao2#!#M5Dv8Q(LLcO;m6}RzAf9_nfNr00|oyrWRoRyiIKOFIxGFL-r5&1$UlkMQw# zJNEWiDg+Ycz55sAhJ60lVZfW+^tt+a?3BzwEw6o~Fs&|ok+~wpy%T>ox8hnBhXdC% zbg%d1+1uWz`e?gSR~Ql4^puv5CnzQ9wO-3P9{BFnu8q?e*e=R#l7eO$O&sZa>l$J2 z60IAp`XP676}&<^>8V!TSa!@U_TF&feI^Qbl3>nH)IeDwB6$0%d{pZEli5A z)mwKOzpG}?hXNN95Zc$_n3y%8aiKQ~ie9fV`W}$t$4lA)DxO%^1j264J^zh`#r04N z6;Cv@lL&TAoHI&g*5_>XJ8pq7yXP-iMe~dE`$f*>qT%VcRD2Bw+oCFMpX6*0|7GIw zaZ2~(bjxbtz)70GHSJI&e=1g9mG=2oq#Dy4x=PafFeGWpac{RgpkRJPTi^OC;n7Zd1Z&-tn^IBZC8SRRLJ;|B#zb0y_vc}Wv`9t zLpCA91M@c~!jtSS!VfF5;@$#j49qykK82aP+0+5C}J3&yR+v8 zj;Zp(0nRISm~op{(sfs-@TgWB_3FcRC|8$!_k`=hF9P*-g5L4qeFV>?qo#rW%}c~d zd+ddMO%xbta%C&5^6#B?myVT{_5tnRY1wUVyBNf0Ga}Ao_gVmp zLq#34BI@c(;A!hD&Og2}ld^D|85ke;12pVbDT1bZaC-fR)O?Q@;DVOP;Ri%ZUJwO| z!4*dbf#HSlbhPnNyky)=ej#b4f9nVT*;3P~@d`1`2ZSbl6^1u)e3E1|woQBmn#T2w zS7fVGSa#>rAqwzPhuV7X`1^*plb3B`Q1|qB>YV;|06E1zSLaSGYyy#;{-To@R)j(!-<$rp=*|N zo>HPa;v=6aB587G>jPSW=Z!t2ih^i$=oP3Ezv>QTXHOHvZNF}WilQ}A8e;tw%s zT5$w13Vm^&(#w{xcW<>W&HY|z4bAd^#`T7+^Wek+HA|MVSM2i&wRwhS0z%R|eb(7- zSi*$nu+}dn*M!UjSgxh6?eShkzB{n8cJbmfB?=L*Pa_7$FEPscb*&X@mg1rHhyHLMkgL%IT&hI`RRFS-SnbNZ1}Fr zGZ3e35w*^Ox7AXa#pn!Qyw@(7Mrv)N^E^WHU!mo7oy|!_*xi*n{Z_!KjsB1_y4mJz zj_I`I>mv$eFdY&TpeH_j*71nLRG2I_HhR5lyqJO6`OQ3VYctTmlh*6Gwv7d*iw=6s zR_*zmnT(NiiYHfdypLzx6duJpuC%8a9{p^thE+CW79-u?hO+IH#veTZT=1utwlP-> zH0Y;{eOye^d#N5a_IrvT7U*RzySh!cN2$CV<7df&c5;YcVCsA3u*(eT_npNovJp;I z_0Wb#-}KtyBBaQRmYwRX@>JhXf{rnpbp2 z`U4}$#^qo*9Q=DQP0_W6ISFN+7I;b258_3V?pizRat7sxk{#i*9}>QI$tF8l+7IHK zDQ0dXRCdbBR=ha?FOjUinsl`k{kKEizFJSwr}#3Hx49wOUfO8uYH0N?Pl_QM3|iW( z_&DHP!3MFeEpX>j{P;t5fqwJor{5x+IA{gPv}Ssws#@Pi@7>3U!X724#5m!ykb^Xe zpCn0KwkBhNV(yj?ARJVH%9uq~Dw`YHDfOP)6An=|=Z_#Res=hy;=L z#qH8u5*-$yqfO47mUD`};cCwOx|j*~Flw!H1f z;6Yzsq{Y&22_5bmyZ~#j4`N7D(i;_<#J74EcE);nNqY|EP=Il&Vo%$`pEkFfmbUBV zI7&WJ)iC~}I*vvIt@0#$rRG&6|A!3eiFtGEiVsSj(fcW7RyX>&MXD%8Th+9yh%?#9d13)mH2YuzLJ< zh`L|LrvAP=VLJ1Dta{D3XArL_;(yxe{B9Gv5He@DjHd1pREu)Y*jk!2Lq;MA`bbQw zo^U&o4gvjy9(4YV=VQcmB!;9P2oN4uQR`F+6#160YoDL^C16Oj5v`gu1&hBq~r2)nszzF@j>aw3@DPub?D+c8!ey49~2 z`i%5k++=@ANYKI+SbKCr0^hOK4`AJRy6nr<$wqq_>^k+NI;}LUNu$Q3mSk9oTut&x zS%}r()Ng~e$z9A1gAfvsX9;B~fL>1hWeM**W4P5wdBc)x$SeGdM{fcHpkW=!DSdta0rYcNzH8{ztRrc78;oT*zM{#rsDOgu9L)c z)%+V}t9R@7EL!DB#kR{Z6as3ZfeC%ohg%m_=ZJvGP^(Xdfi6qdH&2729D+J!E~LEg zbbsO|{dTl}#3ckWK0vtND=uFl*P-xJSCrB4x0z6r6|^xiYP*)`&Oa;h1Q_o4uj5Sf zqi76$9HxHD?PU?r9sLIj$)kcL$HRSFdEoRTpP+(@{K8LZ*-Jf#wEt@BD>BOG{98qOI_S;a4vh9h~$p?jJ>vF3H{$w$o=jqV9dii>AIMQm<%(K3NNT>LxH z1?}NOCwziq!^{Hwn>WHiIIEM(KFHv*p7pxwQ z;C1PJUp8w_p1++^rdDn$H*v4^tTBv1+E0-02tPEdVP!=;&>B&m?Xqy8sJ3A6b)^+B zc?}wi6IuwdUg~(T90f}C+u-c%^{Sj-0qsZVI!<^;Vw(z_i;Cg}y9&u;<)z<0TpA@F z>oDaDLLI~C^0)A`pN4-UwfdvnZN_}kjNdFYrq$L^NnOiB+y0G~A8gFBF2q02z zP>2|n)VH)mX{WKIp=%k8^JY0d{BPXUA*L%#D$Vz?M*8=%g0ojj<*~nRllKvs-*1{D z&FK6IlzSH9d3O#tyri2ZUQ-#Kvu@IJ{x(=5szK4uGF@Nw#2YJXzPWkLd^hMgbZXH) zg=b%j122$B4Cb8Wn#rCi>+H-mya?wCNzeF;dJQ$|o6FcSiGzug?vEG>FF!>S{7img zR@e9S$uDmCx`*craI*mSEx|k^!GHvs2$rfr=`41wGf)-HDpnUe8+%W;!-851vz0ozuIk6NuXR>qjvjXUP9ih zt|eoUEClg0c77ME*{Z0kBD+*kmR5fHit@wK%3`=gC3(1TxqeOZ5c0WXJh=MyPeS$A zTa3d`t1Abun)}eK>o>Nzy8HV=_hj0Tjaf$`I{1CKvMRl+Pke`Y{R??Y`$v z&%AlW9W*9@gBMW~+vk!eJ4CC0&K1HH+J_##yU{}M6|LxinG#DHQ%mjy5Ggk{= zw`oRxoW}qz6b3k-kXi{>2|SKQ=I!&6jy>}Iz?RTzvzDQAa0-^zw;Yk3=56$~c5;2o z9PCWVmm@xQzw}t|j7y2l50_T)pE21L7cIM^l5Wu`x|{XuXefASF6p?1gH;Xbi@b?;C=Hn}2hnx6_#YYvYg`gAs3!i$^T8QYdn!lpspiP@_m z>2eA;zU*o*HNLW&QV_1nS6ys4ys}Ht(^a_s1OR@c~gi*-EWlR zxznE}zE9YByxO@zkPH_UyU_+DYR>s|S?HCG4GssEajyGDtr0YctCUj)?}f6zV{Wv2 zY$3x8qK41})w*_rkyZ3M{KhnKTnb3G7jU*<_+>;$FZ(oK&H(BEx_TWnC3}L+PM$MR zqDQ@b_QXdg!brQ_zFlqC+TPkeQf06>N^;WW@|@Rk_v8(Fx(BEhzJUhy&udPWO+ zw%)(K;ygw*w5BdS(~XzB=zT>xyDhA8O&>%=LL(WBh>I@goJuUpJ)`g5&FK`@KB{fMVhmkW6uO{Do}#d;hd$q?L(q|6HZT*6*1L0-M;B~N3WNt_ zUNl)7uv*Tius3hU=d6unqH(fC1eqc4gW?$N)!Yqz^JmcDt<+j<-PYdegG}+g(-b%O z-cYwD$$7N9%v70JxV^o5;ulf$AKx|{4nSB$A5(>izHx*mDR0rOrfB!kctd0)p%nsM zXt;9`%j~qYL{=a%K3gS21#9(lMa(94B#dwYm3VZ>d((*QyUU$y{H5vfLOhAr1fZd& z^?UM$g-UFs<+cvBu#SLao#)}w9lii!XF0<<-zKoDKC+0ZtHm?qok>WruX$3atJzOj zwbJ-nG5KMi|LHUHTaPFB2PJd7-Og0ONMT|2cmmzAK4#}_`7G9_3q{FyU28pIIpN@&Sr9fgMF4?Y^&ZY1 zdGEnOnGBjbhANkZDC0$RpU-`z!0SMt3FNTZ+nhTr#w?Am_V?Lkbcn|_q*uhrwU&X= z&fb%$$z@7)U4q|S#?yXc(c_=?m(0&!;n<}}gP&&DkiLDO}F+Q9G>@}AM<8L+@aX?53WSu?Z<5HZyecoX45)$a&! z8#$Z3YXi}3;W?orkq#R&8im7TbG;f~6XGA=)y(l-6YX}x z;II+08Yyd)y#?yV%uwEjNRDB4(5wKA_a^@Hkyrzn(G}8_7A+b8%)RyeYV_CPfou7S z;DUq4osRV)!mV~v>zsU7t-OqI)R?ZWW(|%_E9uUcGjrChAyG&cP1IdUGV=vtdVvy6 zIDtQkeL35OSE7WEjHM0inb8T+i&z1{v!$`{z=@P|XOR63fEB*@I_kW-m&l;^xJpH4 z1)4x4)dNA1-Mvwh)cjgDlN?e;%Hymfn~gyb+8JcCsY!B+Fyd;e#UGAw1q*kS#2LBu1M#>8(r%Csf(g`N3EeAhC)n6waG?{MR%XVK(R6g>{hHS zd$3gTN3pkNm@LNNk^z8(j#Q@WF5t9aAj4NDl_c!F^MUB)#G^#Fx=%7dj+mQl7%uf0A^{*HF zctg&6(A|i2Y>D3YCg4W`XNon40zCJ%RNGJHEBy{&HInzqSr@;u4xYQpT{F~rRv2x{ z+rSN$5$>E+^AF5^R6_9XpIYg1qnzJy#St?4b5DajR4&5I2bF5gmElJVJ8~f3zBK&6 zC+@g(NZ-Dzk)q5qG73#iauSMFQn@oP{$V>I{~6_Ve1wd7J|%8DZ=o^P-&z3%)C zDU;gM?VfzuPv7mWjNbPPiVez>q1s72H2u0@8WcMi`l=$edet)$nu1Cnj4VsK3qGzYK`AG$-HSMGV!GwjrQxu z^pTbeP9r}Dax`{rpspUe-U}OQcCtsjlp35dT>=pW{;DMMGR;LrK>Yk$eXvSH#T3T3 z7}_SjppPO?s`MMQ&WF!QU0UyO5a0Z@j?D45|83{MFdeC8T92=<8d7lU9zuOt&Tiv? zu7luzV)2__Co0?^r>y$aTB(hlbe28Ma~nzkThEy^3E`Q%+tZkCtzUpwFR|Edo0yRI zy1d?1cUih(YJJjXSC_jcV_2pM(W77<j}ybisbr)p0#l4pP;AQ_rd*7_N?)T^fl z&d|4@)qGa!Ho=*zfOAv#8YZ`a;Mcj-E7xy(y+CBMvlmmR6HmN1&~``N{qSon-ibtklyq z{pv&HHr#NaU}ZvhSh3#>M(D%Dki8qoy~hWqF8-Oiex7*xp7iId9oql+isHuc?iaS6 z(8Gjyg9_*f(7K5Z5GyB*-){PZ?S$7GLzb-& zxsTS=2 zr)d^c6L?tC^6}7t!8#|7*>(2ea7PE!gK18^Ngs!(NrVjm8Y-&9I|je*JBQApy0nuT zIkffd?V)XR*&Qagrp7!zALgnNtM~oKA3ZgeKyE76^EC%(AUE3)5jk8wj(&;oTDXIv zlWpzh^KAeSWG3!#wri70-K0F34jsdjPFvUayleAWw55`XRsfT*0IF1sw?m~_1Y^Vj zn|$?de9IKS*QM8X9WaCJ*W;GIwTO9`)*mrDaC+>*TDY&A+x&BsFRCemVo&2nNc0J} z=?!Ko7ipBaj29ZkLDUxp-H3}(#sEfI4JZ^pNPD@kn$a#-TE5+tI7{~nTR&-G@mcs$ z6r*`RAve5XC-RYp0>SFnhC&!x*$pE8_nxZjfcPt$fWBmE}ysNVDd*e@u9Y>qjYdN_cxz*&i z%bQ%wW35Bvhj^pSlM7?ZUC131CsNZm$t!62S$Jy9;JDdYN<*!}1zN+mCcUlIlC?|+ zjTk zhESNeAD5UYrG5EL2j3D_apNRy2gzE#Z4-XKHP%~Q|3cehVn`%d&X-tbkSos+zKk(& zs$rxsoe@--;n4Crr>)J=6^`nj8Sr!|yfn8I#IlkUa4mTD_0!~(IbSGVxf~fZSGOB@ zn)>6C`+mISg)Kw2orgxavfPZ5L6^y3b$eI)K%EqGLlm4oNr7)0ROq=k518d1OUO{i z6r4{d-#2z{;M(XJ6hrpTBKPP-zU=6f;e z?Vbjr&#AolKFfES3^~smQI$@aT6M}Y-ubWlq>r<0gKp!}+ZS>-_w81aW(&6oRmxh)Ch~*Jj*6;RSx&= z$-p9Otz=)oT&HucdR(B6N9}r${Xs)mnSlx;;TI04P%A-TNNV_n&e3BEpOa*jO7pdb zvWoT0cdcE3v%~!z0g^UcQjkI%W*h(b_%CTG{IEdxp3xII<${ zY->wYyYmu$oU_NApaol!Re8r%>AOyn5-|VjVGl1yfg0jzR=`JN3C)tpnKGO7&biJj zc-<4TO)f>ndxkLA`k~p9KdoklOWMkH0KG=99D0fQ`|p3TtE6dkpF?sH*L(shMz&poIkyUT3Yk1BY!bbNHMUu)CO2m zRGnTo=SI9m|Bh}AcbO-gL?RUq7^KlL9Py03o4$-_oMlHs##?064D#6Bpi37s+Tb|U zk7cUM51Qx?U&YY2&WlPH5Zx%D<-@e5kcRc0b$4FzqK!A^QIm%qp`o~E55NhQ)WRgEpGr9FrkwmS8VAP72~{p zBjdRH*+7fenH(E3T)!`q{WnzOAD;+JHsq!FKu)V()2HPv90<(7fDg?Y9T4oPP@LlV z1nc4l;e=`CMy067bG1UzKy4+pK)9|Jb%4f6{M#5Zv-hG%Uh>+$u5imKS{s$3uSl)p zNw>IP$%f|xDbuziT&gmGep2c-CcV4I)u9l({b#(6fNg5MJv~ho_2>&gy-ddFudc{< z-;7BacH>1lVpfN+^^wjMlzCTq`M;}m-+uu*(!EW)-V0NG!06*sQ=v3Qn!GZwxy@@b zl=lzVENNadmIRy)^h<(nxz_CP>YnF9ZfQ^z0oOzcd)vhL>$1aet&7)nmnwX0^W>#5 zUW4G@aZZV}9>owG7*soI79@o~aZ*V3`<8y(F4w!a!+<~v+q@E zRdxQA*719=mwozL2E4Qwr{V#i3=UdWa z2d(+4AV^KLWuEa}+b1-?blUC*xipiL#=pZm^uwvNJ9auIvfK!1pSOzngM~d;WI2r63)6IprZ!0rV;6hK@koQnV^XZ&AvqhNo>UwHe^=d9umfu}BWz)x(0k)@ES zNyDu+uLCo2LoG&@;N2)kCw6VYNG>UAs{?=Kf-N}kAKyH3hK)Kj%g?$*;GCtn*29gU zW0@`HwxtR#pd|d|FXPu6OT%;h0)FlWwad{f?&hs6L|X_$+U?K;OpPT&XL+vjy{X&y z$f^g01ER0dK>(As`2fh{gmVutV{Rs%^giGItJ;L)K38(zf9@4j^+CPt;yC376ywwl zJJoCl-A*XZsSD%vzqQzj8n12AjjWmqNlqGl4V?bP{!t+#ifnjtdZRnw>#NElJj;li zNd=N-=TC$OZw6slnNWqhOXk;P`e~q{+@_qdIe(wIvYEKob`)AT@^b@)TPEt&3k(^@ z>!74xwQY_C>g>UOSEn-^sHBQ|nZZMbB}V|j0p~p2{SCxmPgyE?;BPh_O1&e6H`~as zXQ#qm_%(O?iN9WD`u4VktIilctL2sZu(@H^fyTp{VA;4-()O8Z++!&+gz6nE^keTO zGRfU+mkn|*>uNGb_{dE54N7<&5R0ZA^}J6>DERM`v7q?x)6y&`WMek~q>KB|e9_>J z4qfnnk@oHJO!slTdU{H_&`l9ixf6=yPL~HE_T(DFDoHGs`*qfnD7lqu?&UVaEZ4=@ zl3R$y*kWexVrJ$#v#s+x=e*7z=g;#x|Jh&m+kU@&-=FvAeR%;-ZTvcSS|wkJDP5`!Y27_ywHm~rkKLN#ktwJczSvGZ*!~Wv-S_MNxHm4k15kR^jD|X>R|ihz71PxL3cPPU!dSu zxsOb$boXoCk+4a2uAWx5kbg@stKp|jl$Wi#pGtpRHYTO1B>3=$7=vfIZq3z&0f(|xaROmr!f zKI?t@#vij~^60sx;puk~>X7=!E*hThl^=G8QTpcj<)jQ}Z8GeR1>EB;5$2}6ct4B9 zEbDKvJxxnzt^N@z;B65)@Cs~Xmt+W3PoTDJ&)}-z$NZjca;AdH&z^_lfBRhqKH2B? z3kO#ZT_XJJVxy<*&~fY9{OP=fjMCk#7Nr<`0o8crjMvSjgvDGx#WdF*jAPq4E%?`# z=7DH1$8Y)w&YPqD2|9U_y?IQ{$F*%jB*4$V&b&8V2Bry{^;FOK^5zUST02SR>xr3# zaOvR?C08BP`g#|*W*f%w1AF9d)oya0FT}T4qa|m-7v8_5oV*&H6+NG#ow0PU zqcSYntFLO1E`@FLf6R(-XP%u4=Zue;c-jS&-6~GZm^fX~kWTVVZEF*oppWedM8i1H z=_;nuH~JY(fd(`ekF4+seLqmnv2GXfdpAU-!9vK-q%Wt{K5$ap)!z+zyLTC@{q0*4 z!^U<5tukgt2vx^H$>Y9_Ce}c1bdc4xr6>xNj-q&D=SKwZ2wnu>TW^;gzuj?8^9|2X zHW58FO8WO{wmab6T>PthJ4$y$u92)E9usb9Q@M~w7(a38PqVjdb|-c zZ|~@Lofpbxx2hzTotfSWPKWp+b&Gg7L{e+&kj?&Xu4cl+6u;|ERC&uQ8|VHC{k{*f z75Lq7Yf!Bz-Y351k5JgTgvPJ|FFfJLmI>&@E7Yruwvxo?K@%q3JocwFps-M+tN`zWgx?z)5Lsqn+!41t938Oi z7^M;KHI!2;x^0UWP1KZvn)lLkodq?I7y_9gp3BE&4_ONOz;=42#dZ|cDeeBNA)o^| z=Sa^b-dE|WSQi`fl6q&`-c81wk<1d_i>*>+ykp(G`-x2u9AfYN&GUm>JF&yQ;$%D_ zBc+xan%-#Aju>0-Q_FStU6^MJqeKQSJUqWBo1Vc9_cyBl*fUt0!v8{zS z@a`cqgnG9(hL}tH?h_Pu^4`iBKpyysGY)eNVJPCeLO4btE@iErgv~PFQE+ z+IDy#hKDHE%NJ8`W( zd=SSG(|VT97~$tB&qN=Do?cQVXN({s&cW>vPf9Zzx7H&)V1)_??F%P@HUipE!-l~&Ok#7zrT*QbKx2U0KdnzKnFA?H3gyQVWoK{Mtc2K?NZ_IVID8Yl zZ`VguJDnwfFEL~x1nw{DKe$T>O+I~tbC2mA6=X;NY02Cvr4qI_yDETJwd8v# zn>7^-Bgn6{Q6ksT>QoA4B;@&MqD|nKA#RsB$LpU}WV8^Wj=lITN$BarpJMf<^(}>yU03z_OX%1Z9TuTx@BlJP2zm zuEn=r{GWJAxqG+VV=P0qo;Z$_*;KGXc9TwMto0eZgRrDiw0jnRigh=Zmc1#ujL3m4 z@UTyU8Bxj(i;>-;WWce`g|JMb_*H@nMf;+qIk708jR7CyqL-~VV^UB5+t;t&loNF6 zi_0(6g%>wq8HJ7f^eH9bmfb8-Eu>jXPR(BfBNcen_bd2Fr5zX9P9TuaL^0s&BXO0{ z#1zm6vn3Gt=1w|LPq08-l$$Q<&^#j`#a#W&e*N0NTB_^ZL@h|;Q&q2ebJ~#zJCXg= zU+qDEVY83lt2-aL>iS3MzMki$!u|Vh|2=CvGLrpeff=D&5*mslFYbyLv8CrzJ8eT$ zn|7T$j_|IMDFhJC!}vl(<#|)C(1Mp7TQ^-UP3QEclE6O2eCePvSVPZv;mB3XdKmEU!fdlJ4&ag^XKvwNY04sSVAH4_ep7$ z9!bV(fcvsl89Hi(wG@-B|9NCI*J^T$jET~OT#R?aW{6DaOYT3<+hDnEJsnvHbZSa~ z)=U;6KcdEsw%7gnmS>DMy2It?w$P(CC!K9ol%o6_NUM)+8llE4?im%y|DV>Z|F!+Q zec>WVe(pR0gesjY&M&gYnKX@v6%u@4Vm33xSP%K1>hI^iv}AQ<>6L7hnYa(@^C?c! zK|NNpg%52;J?5Me0Z4xg9=yl5QHR5xa9xSEeFBT!-!b2djsvVe;P>FDdow{*;c16H zVw*pcga7ff^(?bkbf@_k-opJQnL^AoeZyCqY^Nllx+==T+Yq3vJ^WM&C69}(;X`(1 zfA0XEedh+Y>3>FY=rQ8owae!vhZ@C?->I)eFBHI@ztA&!DRyjR3!~MVo`FVc7iQP( zE@3`2V~2cKi=bPC3D$cGeCJejy(Ilq&&YnXKYACXS9^5HK8W%vj_EUXzm(gn62)=Cg+0MmP4fX}@@=i(_Ov`^}O=wf3`TgF4=n-0gFGIS=BKUzX*0^EDUK8zv84z-4GJq8#K z+RqRk%N`Mo$Vv&Me9m9zWFmd)p$3I0nC|PXFV9$W!B`SY-Xh8x_hD=#W1`viDy}g) z1)k{`uMxPAo~pX1G0y#;1EOf~zXjtiYo&hJU zW>V~R;O_*q*dZSgV_IkA=_XX9q+EJ%-JO5>U(dhUO?6$nD*5Vpm6(XDM!}tOI&R=Q z$%lqxVmw>yNsn2JQ87}iMtU}41CZ{&LmkBzaktIsaIannTC zIJp_>F*$f_9SuZQ4WN0ORB-+J1~^^@V)H@X^5l1i=#M7X4Qfq0^54OYgBXJ5@W$x{ z`ySq@9E&5bzL5$qh2O_`#v5xp9U+_MW`CNKP8OlN6b zL49zplJ!n2Z+7>wjOuem2mA6N>07^_e15wSl-inJp9@9@^YGpkJU2c*81ptVq&r9U zo1x4}!!|9J?YpwMG?Z?@2VS@R(-+L6;YKGY#%h};C%MWCdgn%-&crk-7;O44dpTLE zsoQ7Uws5xyVB$<>vvXb`77p_>C@L~oKvpjAob2)m3oHLUP+Ky5@qJg4ZN+}AZ<5E) zz*lX5hUpt`Ku8nnn~-ox`>yW5*m=~E>bk3z`o@|A z^M+t2?7o8jGt%imJ%AF*cy|l4>~V6M+p23@C~0d$DzQ!G7s>gGXvP_DlE=>*yO^Wr zLva2qpeKy*P^dIz}o79x4 z4WemTglfhJy-kS~u?g2~fiWbh2YzPz^kvhWt(5E}h;f4I=UNG-J`9ok;FyNea z^Gd1{&+w>Pz!=@s~XzAA9f(z9wwk6 z%Bvo#`c*M7@8vHPX$N^qw43bJnS(wPr`=d$uPSnbpDyRdte7GR5aT!7fqoursd&xb9*D>emH&gU#h+D9WKKk=HA=V7m!zJS_oXwSF*CER&%PK^~>J_T1mjw0O zFZ~O}MYj6lnf{{F`eVdPy~{L1y6{b)VSO`dH=|aEJPMp||6PevQvqhv`Wqz@Jy~sS zks&n}U!_mjt!Ilq4qI812;2QSG*c_OzCG%kmS?>I$=*%APX7Jz_R#>n*&k+y?`-Zq z4*;_N4A>iDN5W+#&x&`roQCM?YW05}ZebvT2DYvtKg=xgbbz1|JL|1B!Q*!xzT|)S ziT14xO2StLMNa1 zpTF8_huyv|xkUl{3c^^wR$ia*^i(q#r5813ZtvFdASX;T#~;lQXR?$0O5gc;o~e&c zj_tKG>kh~i{(D=7vHpYfJGtXSf=$Qe{%k^&g~F%G`Xvm4fI-|!J?SY$Xu>oJ$gazT zQ=dpChP;TO@|4?b*vx@nE@#}4ossYMd#|N00{eyg&WmHnpCsjib3@4NX-(B2%MQ6u zcrLgKK;yq7f5j+tZJ4#1fmIO1u?R#Igs^TByJo@MU}v}6gRO&RWF;B0Po6wE$;q>m z+CjMm;3NhEMTihj9;=&vQ?q8c6Yly7)qLx^!}>Y@!R76Sl9nYuy!X%PaB}#QjM8N= zxMgs4ne4+tF2LocK^!%DijnEHI^W6TeJcbuw!b|<hMne% zw=RK?hr1(Yw`A1Tsu#B%ImzY)mOp1SR6fr3mzl`qho#w)tIBsmogy``Civ{PWA2e| z>+MrhRIDbJVh#0&Hxk-`_w{n^)f(WBIPt53Jisiwi&Hp@V6CKNC(zi7Aoy;Xx* zgx>ZarxP7~eitP|f z$b;CH^>wGlEs}lz696xHhtsVKLI&gnhru&8Ho@Dv@!v>puc{Z$Y^2e5A{n#c?j>@c zBz~(64_wu2B^-bp(KuZtGWcqyveo!bpT^aI1<=@U62$`l>D-9o2oUSkP+i&x8s=YP zZWu_3y(FBAIe%bO9pMX-<}}>?K%O*qol6Ki4+)NZ^?LsI4Ibzr;0(@va^H6tdLW@Z z-GfH_;UgG8g*4aBy_K;yE9})S&wncgDz{x z^-(-wV@VB+Xyn%a3WjmiQ<(~DpVDRplAbe~iR2-{uZMoOJ8uu}{-e0R{%*RoqTqGb z_U!hm6gyA8%PRk>ev0t10jGh{AD7bHP#cz(RaG?TIL$viyyirw6|bhcbyN}0^jhFy z&@FqWafGQkAOy^{pwp=ZxuQ91EOS`<8rBv2=BJAAi%I{K0t<{kzlsqAIT!A!@=3KJ z=rTr9Sw~!A;Tkn;6bmH~w~QLU{#qu5p*&R3>Sldpk17o0JGcKUWDPP93@quVC!90Z`>$XP?Y`Jsm9c;8&(qyn)snGvpt41P?TbF*A(lDg3mqC}ZeU9$_QcwY z*6dQp7?3N;pc&bYWRX%A$u45j=G@ah{?FTWvPH4qr32M)BC`jf$+>giuezaie^gR@ zsDg=Q>?nA!T~J}5Hd#Y;g<7h^-SE!KQ8uZ}+C9T%At_j*si^``0>{IRb{)o$mjLaTtfGfJkB8*amjH?(|@+0Cf$~|c*b#yA7>Qjhh8*jt#2Z{Ax>p=O0Jxx7`gkE|laV_l7*io%R?Yw4;t6rs+EeTl( zyyvRbz$jZMifRnNuZ{wjkoz3uhy82yy1r4)SyS#AL~k|H?&OfAAC!fsB65gMC4mG@ zQ2a7+0}|oof63_b8_uc(P^^)mQ0(Ep@@S;2(pHyZgHVGtOJqo|H_I}QF7luy**0gV zk-Sq9)63UcDmeVWDnFQK77;W!Nz(@EX)6A;WT=_!)Q+Az(NqWWXo4&p-fKxF*#8k) zM)_ay+b{=>h5#~#;j>>Sv%yLv*5{2AJ>0>`0t9Den+Eg+Q zAd)r~imPSr;H>UNKFf`a-Ufb%#KhslfUIqk_nMOKntZ=a)gjbJm!-xX6NG8#$Qd)~ zP9MmBztm28sl8Olx46{wZ6rg%Cu(w2Gi;1O;_+4;rUwg~hA?<;(jh0UoXha2b;P+X zt?wiC3$#z9XMeo?F} zdupYs{>>@M-KS)U^nDrSWQ3B(z(WZTNKwupgm;A#g?b0$iBD7OPFLujbF3~^>6O!z zoLWR$&4%AqH&ZY;o##u*xU|FLi)jJOPmz(p4sMDm^5?->D~oGV_V4;Dc$d}=th{>A z8#Im9a+dp9eDoMiTI7;SVG)rPH5YHszz);y;R ziwqmk3Y{Ke!_hrjDnhl#k7jz}3d@^PAh*uUm;H@~sv=SsutU!xuW5@t3{wem2fKjh z2|+WO36QlrvM&n_Ewj|++Fc)}iOk%VeYuG`EQuAmdHDl1f9m`MdHI!74M0ZhYi#XD z6?=p~t}og9z9R2@;>Hr6znNyBae6#fgm2BN*v z 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

0uun9MN@v_>mi!p;>-tqJz@tf?`uE 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%0A0=N`Y~x;J0f>g7{$%89%Haw6P{*29oFux zI=tH9<0NdXpzFOwIZnPg8w361yesHdwXNZ=08e6Bg4?uo?~1 z>D56nc7s4Z6i*sJbZLW(RM1R`Ym_$7q0om$4@JuIELSx69>-le*7dK%@Gq@i09)_s zQ`a$Kq0uexog5Nmjk}8dyS4AMJ$zWhUx66G7Z*1rF5HbwF5Y`})VY8~PLFrq^+lP1 z{vjYrH3v+%Sm|#~;bV+Nd4DnkNn)uGj&Wx# zNJVR{+GyQRIR#X0gJUY2T>B6NkF^>x@T|&hrnsrc#UG~*K%l4_vLc{~M93F#GxVG) zZ5h9Ign5rZ;@Z~~!q?iGY``L1!z*W!bI*SN>?E&uNVFRP{e}+7eAsVa{yr&qp+64!1CD%)N=gI} zpDYOHguqwrBjCxEEQQk%ohDBL%$?fFDci2(qko z&32xmy})uxv<`-Sj?Awqr^0tPiQM!vK^ zGwSP5Lxq?8nQ-|OYggq1iRQkuJFPtTnGY#d^XZx0DfdT!=kHweHO4YQ(#%=EzvjLR;+fq=* z()X;&ZRBV4hv)xw!^QtRw{%!BjUcp{lSd8{O5xW0Cl^$gcO~w4{CmLM9qAqtm$n_H dRJr+h(&=V{Cm!|vkDCAaIsVs^kkH@5{|$~L%@hCt diff --git a/docs/_static/logo_small.jpg b/docs/_static/logo_small.jpg deleted file mode 100644 index 773412bdba1c18e24eb1b6218349c1fa10f22470..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8158 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 T)3?7#fg3{n-;Xxy$A$j~iwSX0 diff --git a/docs/_static/logo_square.svg b/docs/_static/logo_square.svg new file mode 100644 index 0000000..bf1364e --- /dev/null +++ b/docs/_static/logo_square.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/_static/logo_wide.svg b/docs/_static/logo_wide.svg new file mode 100644 index 0000000..f0f3f9a --- /dev/null +++ b/docs/_static/logo_wide.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/conf.py b/docs/conf.py index 92a8b1e..aa62430 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,6 +34,7 @@ "repository_branch": "master", "path_to_docs": "docs", "home_page_in_toc": True, + "logo_only": True, } # Add any paths that contain templates here, relative to this directory. @@ -58,7 +59,9 @@ # a list of builtin themes. # html_theme = "sphinx_book_theme" -html_logo = "_static/logo_small.jpg" +html_title = "Jupyter Cache" +html_logo = "_static/logo_wide.svg" +html_favicon = "_static/logo_square.svg" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, From c22fceb10b84c1482efc7a0b511998c04016fe5f Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 3 Aug 2021 20:04:49 +0200 Subject: [PATCH 09/39] =?UTF-8?q?=F0=9F=93=9A=20DOCS:=20Update=20introduct?= =?UTF-8?q?ion=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + docs/conf.py | 8 +- docs/develop/contributing.md | 11 +++ docs/index.md | 101 +++++++++++++++++----- jupyter_cache/cache/db.py | 2 +- jupyter_cache/cli/commands/cmd_exec.py | 7 +- jupyter_cache/cli/commands/cmd_project.py | 13 ++- jupyter_cache/utils.py | 9 +- 8 files changed, 122 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 4e25241..56e666a 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,4 @@ _archive/ .DS_Store .vscode/ ~$* +_*.ipynb diff --git a/docs/conf.py b/docs/conf.py index aa62430..3acc6d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,6 +25,7 @@ # "sphinx.ext.autodoc", # "sphinx.ext.viewcode", ] +myst_enable_extensions = ["colon_fence", "deflist"] jupyter_execute_notebooks = "off" html_theme_options = { "repository_url": "https://github.com/executablebooks/jupyter-cache", @@ -109,6 +110,7 @@ class JcacheCli(SphinxDirective): "command": directives.unchanged_required, "args": directives.unchanged_required, "input": directives.unchanged_required, + "allow-exception": directives.flag, } def run(self): @@ -156,9 +158,11 @@ def run(self): finally: os.chdir(old_cwd) + if result.exception and "allow-exception" not in self.options: + raise self.error( + f"CLI raised exception: {result.exception}\n---\n{result.output}\n---\n" + ) text = f"$ {' '.join(cmd_string)} {args}\n{result.output}" - if result.exception: - text += "\n" + str(result.exception) + "\n" text = text.replace(root_path + os.sep, "../") node = nodes.literal_block(text, text, language="console") return [node] diff --git a/docs/develop/contributing.md b/docs/develop/contributing.md index 3814a5f..d6a6717 100644 --- a/docs/develop/contributing.md +++ b/docs/develop/contributing.md @@ -6,6 +6,17 @@ [![Code style: black][black-badge]][black-link] [![PyPI][pypi-badge]][pypi-link] +## Installation + +For package development: + +```bash +git clone https://github.com/ExecutableBookProject/jupyter-cache +cd jupyter-cache +git checkout develop +pip install -e .[cli,code_style,testing,rtd] +``` + ## Code Style Code style is tested using [flake8](http://flake8.pycqa.org), diff --git a/docs/index.md b/docs/index.md index 95d9435..ba0b41b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,86 @@ # Jupyter Cache -A defined interface for working with a cache of jupyter notebooks. +This package provides an [API](use/api) and [CLI](use/cli) for executing and cacheing multiple Jupyter Notebook-like files. + +Smart re-execution +: Notebooks will only be re-executed when **code cells** have changed (or code related metadata), not Markdown/Raw cells. + +Pluggable execution modes +: Select the executor for notebooks, including serial and parallel execution + +Execution reports +: Timing statistics and exception tracebacks are stored for analysis + +[jupytext](https://jupytext.readthedocs.io) integration +: Read and execute notebooks written in multiple formats + +## Installation + +Install `jupyter-cache`, via pip or Conda: + +```bash +pip install jupyter-cache[cli] +``` + +```bash +conda install jupyter-cache +``` + +## Quick-start + +```{jcache-clear} +``` + +Add one or more source notebook files to the "project": + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: add +:args: tests/notebooks/basic_unrun.ipynb +:input: y +``` + +These files are now ready for execution: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: list +``` + +Now run the execution: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +:command: execute +:args: --executor local-serial +``` + +Successfully executed files will now be associated with a record in the cache: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: list +``` + +The cache record includes execution statistics: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: show +:args: 1 +``` + +Next time we execute, jupyter-cache will check which files require re-execution: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +:command: execute +``` + +The source files themselves will not be modified during/after execution. +You can merge the cached outputs into a source notebook with: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: merge +:args: 1 _executed_notebook.ipynb +``` + +## Design considerations -This packages provides a clear [API](use/api) and [CLI](use/cli) for executing and cacheing multiple Jupyter Notebooks in a project. Although there are certainly other use cases, the principle use case this was written for is generating books / websites, created from multiple notebooks (and other text documents). It is desired that notebooks can be *auto-executed* **only** if the notebook had been modified in a way that may alter its code cell outputs. @@ -19,24 +97,7 @@ Some desired requirements (not yet all implemented): - Store execution artifacts: created during execution - A transparent and robust cache invalidation: imagine the user updating an external dependency or a Python module, or checking out a different git branch. -## Installation - -To install `jupyter-cache`, do the following: - -```bash -pip install jupyter-cache[cli] -``` - -For package development: - -```bash -git clone https://github.com/ExecutableBookProject/jupyter-cache -cd jupyter-cache -git checkout develop -pip install -e .[cli,code_style,testing,rtd] -``` - -Here are the site contents: +## Contents ```{toctree} using/cli diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 2e6d1f7..70c7937 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -251,7 +251,7 @@ def format_dict(self, cache_record=None, path_length=None, assets=True): "ID": self.pk, "URI": str(shorten_path(self.uri, path_length)), "Reader": self.reader, - "Created": self.created.isoformat(" ", "minutes"), + "Added": self.created.isoformat(" ", "minutes"), } if assets: data["Assets"] = len(self.assets) diff --git a/jupyter_cache/cli/commands/cmd_exec.py b/jupyter_cache/cli/commands/cmd_exec.py index 311caf7..c0f41fa 100644 --- a/jupyter_cache/cli/commands/cmd_exec.py +++ b/jupyter_cache/cli/commands/cmd_exec.py @@ -69,7 +69,8 @@ def execute_nbs(cache_path, executor, pk_paths, timeout): "Finished! Successfully executed notebooks have been cached.", fg="green" ) output = result.as_json() - output["up-to-date"] = list( - {record.uri for record in records}.difference(result.all()) - ) + if records: + output["up-to-date"] = list( + {record.uri for record in records}.difference(result.all()) + ) click.echo(yaml.safe_dump(output, sort_keys=False)) diff --git a/jupyter_cache/cli/commands/cmd_project.py b/jupyter_cache/cli/commands/cmd_project.py index f192771..2e84a22 100644 --- a/jupyter_cache/cli/commands/cmd_project.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -77,13 +77,22 @@ def remove_nbs(cache_path, pk_paths): # help="Compare to cached notebooks (to find cache ID).", # ) @options.PATH_LENGTH -def list_nbs_in_project(cache_path, path_length): +@click.option( + "--assets", + is_flag=True, + help="Show the number of assets associated with each notebook", +) +def list_nbs_in_project(cache_path, path_length, assets): """List notebooks in the project.""" db = get_cache(cache_path) records = db.list_project_records() if not records: click.secho("No notebooks in project", fg="blue") - click.echo(tabulate_project_records(records, path_length=path_length, cache=db)) + click.echo( + tabulate_project_records( + records, path_length=path_length, cache=db, assets=assets + ) + ) @cmnd_project.command("show") diff --git a/jupyter_cache/utils.py b/jupyter_cache/utils.py index ffac542..9c86612 100644 --- a/jupyter_cache/utils.py +++ b/jupyter_cache/utils.py @@ -91,13 +91,16 @@ def tabulate_cache_records(records: list, hashkeys=False, path_length=None) -> s ) -def tabulate_project_records(records: list, path_length=None, cache=None) -> str: +def tabulate_project_records( + records: list, path_length=None, cache=None, assets=False +) -> str: """Tabulate cache records. :param records: list of ``NbProjectRecord`` :param path_length: truncate URI paths to x components :param cache: If the cache is given, we use it to add a column of matched cached pk (if available) + :param assets: Show the number of assets """ import tabulate @@ -107,6 +110,8 @@ def tabulate_project_records(records: list, path_length=None, cache=None) -> str if cache is not None: cache_record = cache.get_cached_project_nb(record.uri) rows.append( - record.format_dict(cache_record=cache_record, path_length=path_length) + record.format_dict( + cache_record=cache_record, path_length=path_length, assets=assets + ) ) return tabulate.tabulate(rows, headers="keys") From 6cae3533be174501e1b8e68e7dedc777d79ec1ba Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 3 Aug 2021 20:13:26 +0200 Subject: [PATCH 10/39] =?UTF-8?q?=F0=9F=A7=AA=20TESTS:=20Add=20CLI=20execu?= =?UTF-8?q?te=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_cli.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 6c39e83..9c26c69 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,7 +3,7 @@ from click.testing import CliRunner from jupyter_cache.cache.main import JupyterCacheBase -from jupyter_cache.cli.commands import cmd_cache, cmd_main, cmd_project +from jupyter_cache.cli.commands import cmd_cache, cmd_exec, cmd_main, cmd_project NB_PATH = os.path.join(os.path.realpath(os.path.dirname(__file__)), "notebooks") @@ -214,6 +214,16 @@ def test_show_project_record(tmp_path): assert "basic.ipynb" in result.output.strip(), result.output +def test_execute(tmp_path): + db = JupyterCacheBase(str(tmp_path)) + db.add_nb_to_project(path=os.path.join(NB_PATH, "basic.ipynb")) + runner = CliRunner() + result = runner.invoke(cmd_exec.execute_nbs, ["-p", tmp_path]) + assert result.exception is None, result.output + assert result.exit_code == 0, result.output + assert len(db.list_cache_records()) == 1 + + def test_project_merge(tmp_path): db = JupyterCacheBase(str(tmp_path)) record = db.add_nb_to_project(path=os.path.join(NB_PATH, "basic_unrun.ipynb")) From a35c364930c258a15d70f60aaca3d5c6cf71ce0d Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 3 Aug 2021 21:11:24 +0200 Subject: [PATCH 11/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20REFACTOR:=20Move=20c?= =?UTF-8?q?ritical=20CLI=20dependencies=20to=20install=5Frequires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conf.py | 4 ++++ jupyter_cache/cli/commands/__init__.py | 11 +++++++---- jupyter_cache/cli/commands/cmd_exec.py | 7 +++---- jupyter_cache/cli/options.py | 23 +++++++++++++++++++++++ jupyter_cache/cli/utils.py | 24 ++++++++++++++++++++++++ setup.cfg | 6 +++--- tox.ini | 4 ++-- 7 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 jupyter_cache/cli/utils.py diff --git a/docs/conf.py b/docs/conf.py index 3acc6d6..2f6cebf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -158,6 +158,10 @@ def run(self): finally: os.chdir(old_cwd) + if result.exit_code != 0 and "allow-exception" not in self.options: + raise self.error( + f"CLI non-zero exit code: {result.exit_code}\n---\n{result.output}\n---\n" + ) if result.exception and "allow-exception" not in self.options: raise self.error( f"CLI raised exception: {result.exception}\n---\n{result.output}\n---\n" diff --git a/jupyter_cache/cli/commands/__init__.py b/jupyter_cache/cli/commands/__init__.py index ea0fe33..b8ff253 100644 --- a/jupyter_cache/cli/commands/__init__.py +++ b/jupyter_cache/cli/commands/__init__.py @@ -1,7 +1,10 @@ -import click_completion - -# Activate the completion of parameter types provided by the click_completion package -click_completion.init() +try: + import click_completion +except ImportError: + pass +else: + # Activate the completion of parameter types provided by the click_completion package + click_completion.init() from .cmd_cache import * # noqa: F401,F403,E402 from .cmd_config import * # noqa: F401,F403,E402 diff --git a/jupyter_cache/cli/commands/cmd_exec.py b/jupyter_cache/cli/commands/cmd_exec.py index c0f41fa..1ab9ebf 100644 --- a/jupyter_cache/cli/commands/cmd_exec.py +++ b/jupyter_cache/cli/commands/cmd_exec.py @@ -2,15 +2,14 @@ from pathlib import Path import click -import click_log from jupyter_cache import get_cache -from jupyter_cache.cli import arguments, options +from jupyter_cache.cli import arguments, options, utils from jupyter_cache.cli.commands.cmd_main import jcache from jupyter_cache.readers import list_readers logger = logging.getLogger(__name__) -click_log.basic_config(logger) +utils.setup_logger(logger) @jcache.command("execute") @@ -18,7 +17,7 @@ @options.EXECUTOR_KEY @options.EXEC_TIMEOUT @options.CACHE_PATH -@click_log.simple_verbosity_option(logger) +@options.set_log_level(logger) def execute_nbs(cache_path, executor, pk_paths, timeout): """Execute all or specific outdated notebooks in the project.""" import yaml diff --git a/jupyter_cache/cli/options.py b/jupyter_cache/cli/options.py index 48aa180..df8d75b 100644 --- a/jupyter_cache/cli/options.py +++ b/jupyter_cache/cli/options.py @@ -1,3 +1,4 @@ +import logging import os import click @@ -143,3 +144,25 @@ def confirm_remove_all(ctx, param, remove_all): help="Remove all notebooks.", callback=confirm_remove_all, ) + + +def set_log_level(logger): + """Set the log level of the logger.""" + + def _callback(ctx, param, value): + """Set logging level.""" + level = getattr(logging, value.upper(), None) + if level is None: + raise click.BadParameter(f"Unknown log level: {value.upper()}") + logger.setLevel(level) + + return click.option( + "-v", + "--verbosity", + type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), + default="INFO", + show_default=True, + expose_value=False, + callback=_callback, + help="Set logging verbosity.", + ) diff --git a/jupyter_cache/cli/utils.py b/jupyter_cache/cli/utils.py new file mode 100644 index 0000000..783cc0a --- /dev/null +++ b/jupyter_cache/cli/utils.py @@ -0,0 +1,24 @@ +import logging + +import click + + +class ClickLogHandler(logging.Handler): + _use_stderr = True + + def emit(self, record): + try: + msg = self.format(record) + click.echo(msg, err=self._use_stderr) + except Exception: + self.handleError(record) + + +def setup_logger(logger: logging.Logger) -> None: + """Add handler to log to click.""" + try: + import click_log + except ImportError: + logger.addHandler(ClickLogHandler()) + else: + click_log.basic_config(logger) diff --git a/setup.cfg b/setup.cfg index 543e217..482a764 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,11 +29,14 @@ project_urls = packages = find: install_requires = attrs + click importlib-metadata nbclient>=0.2,<0.6 nbdime nbformat + pyyaml sqlalchemy>=1.3.12,<1.5 + tabulate python_requires = ~=3.6 include_package_data = True zip_safe = True @@ -50,11 +53,8 @@ jcache.readers = [options.extras_require] cli = - click click-completion click-log - pyyaml - tabulate code_style = pre-commit~=2.12 rtd = diff --git a/tox.ini b/tox.ini index 0bb66b1..4b101bb 100644 --- a/tox.ini +++ b/tox.ini @@ -17,14 +17,14 @@ envlist = py38 usedevelop = true [testenv:py{36,37,38,39}] -extras = cli,testing +extras = testing deps = black flake8 commands = pytest {posargs} [testenv:cli] -extras = cli +; extras = cli deps = ipykernel jupytext From 6d780fdbd6bf337f858063d55921d4ed2c92fdfe Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 3 Aug 2021 22:27:04 +0200 Subject: [PATCH 12/39] =?UTF-8?q?=F0=9F=91=8C=20IMPROVE:=20CLI:=20Add=20st?= =?UTF-8?q?atus=20to=20`project=20list`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 2 +- jupyter_cache/cache/db.py | 10 +++++++--- jupyter_cache/utils.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index ba0b41b..7356858 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,7 +35,7 @@ Add one or more source notebook files to the "project": ```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: add -:args: tests/notebooks/basic_unrun.ipynb +:args: tests/notebooks/basic_unrun.ipynb tests/notebooks/basic_failing.ipynb :input: y ``` diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 70c7937..0bebc10 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -247,16 +247,20 @@ def to_dict(self): return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} def format_dict(self, cache_record=None, path_length=None, assets=True): + status = "-" + if cache_record: + status = f"✅ [{cache_record.pk}]" + elif self.traceback: + status = "❌" data = { "ID": self.pk, "URI": str(shorten_path(self.uri, path_length)), "Reader": self.reader, "Added": self.created.isoformat(" ", "minutes"), + "Status": status, } if assets: data["Assets"] = len(self.assets) - if cache_record is not None: - data["Cache ID"] = cache_record.pk return data @validates("assets") @@ -341,7 +345,7 @@ def record_from_uri(uri: str, db: Engine) -> "NbProjectRecord": @staticmethod def records_all(db: Engine) -> "NbProjectRecord": with session_context(db) as session: # type: Session - results = session.query(NbProjectRecord).all() + results = session.query(NbProjectRecord).order_by(NbProjectRecord.pk).all() session.expunge_all() return results diff --git a/jupyter_cache/utils.py b/jupyter_cache/utils.py index 9c86612..c446499 100644 --- a/jupyter_cache/utils.py +++ b/jupyter_cache/utils.py @@ -105,7 +105,7 @@ def tabulate_project_records( import tabulate rows = [] - for record in sorted(records, key=lambda r: r.created, reverse=True): + for record in records: cache_record = None if cache is not None: cache_record = cache.get_cached_project_nb(record.uri) From 47ab9418418ee2e918c67493b1f3f410ea14b80a Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 4 Aug 2021 04:51:36 +0200 Subject: [PATCH 13/39] =?UTF-8?q?=E2=9C=A8=20NEW:=20Parallel=20(multiproce?= =?UTF-8?q?ss)=20notebook=20execution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The execution logic was also refactored, to reduce code duplication. Note, artefact retrieval has been removed for now, until the logic can be improved. --- README.md | 2 +- docs/index.md | 2 +- docs/using/api.ipynb | 225 ++++++----- docs/using/cli.md | 2 +- jupyter_cache/base.py | 42 ++- jupyter_cache/cache/db.py | 1 - jupyter_cache/cache/main.py | 19 +- jupyter_cache/cli/commands/cmd_cache.py | 2 +- jupyter_cache/cli/commands/cmd_project.py | 4 +- jupyter_cache/executors/base.py | 22 ++ jupyter_cache/executors/basic.py | 432 ++++++++++++---------- jupyter_cache/executors/utils.py | 46 ++- setup.cfg | 2 + tests/test_cache.py | 25 +- 14 files changed, 498 insertions(+), 328 deletions(-) diff --git a/README.md b/README.md index 57e99bd..26dc5f5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Some desired requirements (not yet all implemented): - Allow parallel access to notebooks (for execution) - Store execution statistics/reports - Store external assets: Notebooks being executed often require external assets: importing scripts/data/etc. These are prepared by the users. -- Store execution artifacts: created during execution +- Store execution artefacts: created during execution - A transparent and robust cache invalidation: imagine the user updating an external dependency or a Python module, or checking out a different git branch. ## Install diff --git a/docs/index.md b/docs/index.md index 7356858..4b2b510 100644 --- a/docs/index.md +++ b/docs/index.md @@ -94,7 +94,7 @@ Some desired requirements (not yet all implemented): - Allow parallel access to notebooks (for execution) - Store execution statistics/reports. - Store external assets: Notebooks being executed often require external assets: importing scripts/data/etc. These are prepared by the users. -- Store execution artifacts: created during execution +- Store execution artefacts: created during execution - A transparent and robust cache invalidation: imagine the user updating an external dependency or a Python module, or checking out a different git branch. ## Contents diff --git a/docs/using/api.ipynb b/docs/using/api.ipynb index 74b8642..8311b9d 100644 --- a/docs/using/api.ipynb +++ b/docs/using/api.ipynb @@ -48,7 +48,7 @@ "from pathlib import Path\n", "import nbformat as nbf\n", "from jupyter_cache import get_cache\n", - "from jupyter_cache.base import NbBundleIn\n", + "from jupyter_cache.base import CacheBundleIn\n", "from jupyter_cache.executors import load_executor, list_executors\n", "from jupyter_cache.utils import (\n", " tabulate_cache_records, \n", @@ -174,12 +174,12 @@ "data": { "text/plain": [ "{'pk': 1,\n", - " 'hashkey': '94c17138f782c75df59e989fffa64e3a',\n", - " 'description': '',\n", - " 'created': datetime.datetime(2021, 8, 2, 20, 39, 31, 350600),\n", - " 'data': {},\n", " 'uri': 'example_nbs/basic.ipynb',\n", - " 'accessed': datetime.datetime(2021, 8, 2, 20, 39, 31, 350606)}" + " 'data': {},\n", + " 'accessed': datetime.datetime(2021, 8, 4, 2, 23, 2, 177344),\n", + " 'description': '',\n", + " 'hashkey': '94c17138f782c75df59e989fffa64e3a',\n", + " 'created': datetime.datetime(2021, 8, 4, 2, 23, 2, 177333)}" ] }, "metadata": {}, @@ -263,7 +263,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - "NbBundleOut(nb=Notebook(cells=1), record=NbCacheRecord(pk=1), artifacts=NbArtifacts(paths=0))" + "CacheBundleOut(nb=Notebook(cells=1), record=NbCacheRecord(pk=1), artifacts=NbArtifacts(paths=0))" ] }, "metadata": {}, @@ -332,9 +332,9 @@ "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mCachingError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_34718/3576020660.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m record = cache.cache_notebook_file(\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"example_nbs\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"basic.ipynb\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m )\n", - "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_file\u001b[0;34m(self, path, uri, artifacts, data, check_validity, overwrite)\u001b[0m\n\u001b[1;32m 267\u001b[0m \"\"\"\n\u001b[1;32m 268\u001b[0m \u001b[0mnotebook\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnbf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnbf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mNO_CONVERT\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 269\u001b[0;31m return self.cache_notebook_bundle(\n\u001b[0m\u001b[1;32m 270\u001b[0m NbBundleIn(\n\u001b[1;32m 271\u001b[0m \u001b[0mnotebook\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_bundle\u001b[0;34m(self, bundle, check_validity, overwrite, description)\u001b[0m\n\u001b[1;32m 212\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexists\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 213\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0moverwrite\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 214\u001b[0;31m raise CachingError(\n\u001b[0m\u001b[1;32m 215\u001b[0m \u001b[0;34m\"Notebook already exists in cache and overwrite=False.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 216\u001b[0m )\n", + "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_34728/3576020660.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m record = cache.cache_notebook_file(\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"example_nbs\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"basic.ipynb\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m )\n", + "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_file\u001b[0;34m(self, path, uri, artifacts, data, check_validity, overwrite)\u001b[0m\n\u001b[1;32m 268\u001b[0m \"\"\"\n\u001b[1;32m 269\u001b[0m \u001b[0mnotebook\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnbf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnbf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mNO_CONVERT\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 270\u001b[0;31m return self.cache_notebook_bundle(\n\u001b[0m\u001b[1;32m 271\u001b[0m CacheBundleIn(\n\u001b[1;32m 272\u001b[0m \u001b[0mnotebook\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_bundle\u001b[0;34m(self, bundle, check_validity, overwrite, description)\u001b[0m\n\u001b[1;32m 213\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexists\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 214\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0moverwrite\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 215\u001b[0;31m raise CachingError(\n\u001b[0m\u001b[1;32m 216\u001b[0m \u001b[0;34m\"Notebook already exists in cache and overwrite=False.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 217\u001b[0m )\n", "\u001b[0;31mCachingError\u001b[0m: Notebook already exists in cache and overwrite=False." ] } @@ -355,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "source": [ "notebook = nbf.read(str(Path(\"example_nbs\", \"basic.ipynb\")), 4)\n", "notebook" @@ -396,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "source": [ "cache.match_cache_notebook(notebook)" ], @@ -423,7 +423,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "source": [ "notebook.cells[0].source = \"change some text\"" ], @@ -432,7 +432,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "source": [ "cache.match_cache_notebook(notebook)" ], @@ -459,7 +459,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "source": [ "notebook.cells[1].source = \"change some source code\"" ], @@ -468,7 +468,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "source": [ "cache.match_cache_notebook(notebook)" ], @@ -476,14 +476,14 @@ { "output_type": "error", "ename": "KeyError", - "evalue": "'Cache record not found for NB with hashkey: 74933d8a93d1df9caad87b2e6efcdc69'", + "evalue": "'Cache record not found for NB with hashkey: 07e6a47c8c180cb7851ede6dbb088769'", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mcache\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmatch_cache_notebook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnotebook\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m~/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mmatch_cache_notebook\u001b[0;34m(self, nb)\u001b[0m\n\u001b[1;32m 328\u001b[0m \"\"\"\n\u001b[1;32m 329\u001b[0m \u001b[0mhashkey\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_hash_notebook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 330\u001b[0;31m \u001b[0mcache_record\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mNbCacheRecord\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrecord_from_hashkey\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhashkey\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 331\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mcache_record\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 332\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/GitHub/jupyter-cache/jupyter_cache/cache/db.py\u001b[0m in \u001b[0;36mrecord_from_hashkey\u001b[0;34m(hashkey, db)\u001b[0m\n\u001b[1;32m 150\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 151\u001b[0m raise KeyError(\n\u001b[0;32m--> 152\u001b[0;31m \u001b[0;34m\"Cache record not found for NB with hashkey: {}\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhashkey\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 153\u001b[0m )\n\u001b[1;32m 154\u001b[0m \u001b[0msession\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexpunge\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mKeyError\u001b[0m: 'Cache record not found for NB with hashkey: 74933d8a93d1df9caad87b2e6efcdc69'" + "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_34728/941642554.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mcache\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmatch_cache_notebook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnotebook\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mmatch_cache_notebook\u001b[0;34m(self, nb)\u001b[0m\n\u001b[1;32m 333\u001b[0m \"\"\"\n\u001b[1;32m 334\u001b[0m \u001b[0m_\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhashkey\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcreate_hashed_notebook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 335\u001b[0;31m \u001b[0mcache_record\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mNbCacheRecord\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrecord_from_hashkey\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhashkey\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 336\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mcache_record\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 337\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/db.py\u001b[0m in \u001b[0;36mrecord_from_hashkey\u001b[0;34m(hashkey, db)\u001b[0m\n\u001b[1;32m 158\u001b[0m )\n\u001b[1;32m 159\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 160\u001b[0;31m raise KeyError(\n\u001b[0m\u001b[1;32m 161\u001b[0m \u001b[0;34m\"Cache record not found for NB with hashkey: {}\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhashkey\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 162\u001b[0m )\n", + "\u001b[0;31mKeyError\u001b[0m: 'Cache record not found for NB with hashkey: 07e6a47c8c180cb7851ede6dbb088769'" ] } ], @@ -502,7 +502,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "source": [ "print(cache.diff_nbnode_with_cache(1, notebook, as_str=True))" ], @@ -554,9 +554,9 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "source": [ - "nb_bundle = NbBundleIn(\n", + "nb_bundle = CacheBundleIn(\n", " nb=notebook,\n", " uri=Path(\"example_nbs\", \"basic.ipynb\"),\n", " data={\"tag\": \"mytag\"}\n", @@ -579,7 +579,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "source": [ "print(tabulate_cache_records(\n", " cache.list_cache_records(), path_length=1, hashkeys=True\n", @@ -592,8 +592,8 @@ "text": [ " ID Origin URI Created Accessed Hashkey\n", "---- ------------ ---------------- ---------------- --------------------------------\n", - " 2 basic.ipynb 2020-03-13 14:21 2020-03-13 14:21 74933d8a93d1df9caad87b2e6efcdc69\n", - " 1 basic.ipynb 2020-03-13 14:21 2020-03-13 14:21 818f3412b998fcf4fe9ca3cca11a3fc3\n" + " 2 basic.ipynb 2021-08-04 02:25 2021-08-04 02:25 07e6a47c8c180cb7851ede6dbb088769\n", + " 1 basic.ipynb 2021-08-04 02:23 2021-08-04 02:25 94c17138f782c75df59e989fffa64e3a\n" ] } ], @@ -609,7 +609,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "source": [ "cache.get_cache_limit()" ], @@ -629,7 +629,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "source": [ "cache.change_cache_limit(100)" ], @@ -659,7 +659,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "source": [ "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", "record" @@ -680,7 +680,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "source": [ "record.to_dict()" ], @@ -689,11 +689,12 @@ "output_type": "execute_result", "data": { "text/plain": [ - "{'uri': '/Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb',\n", - " 'traceback': '',\n", - " 'created': datetime.datetime(2020, 3, 13, 14, 21, 47, 304914),\n", + "{'uri': '/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb',\n", " 'assets': [],\n", - " 'pk': 1}" + " 'created': datetime.datetime(2021, 8, 4, 2, 25, 42, 410359),\n", + " 'reader': 'nbformat',\n", + " 'pk': 1,\n", + " 'traceback': ''}" ] }, "metadata": {}, @@ -711,7 +712,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "source": [ "cache.get_cached_project_nb(1)" ], @@ -731,7 +732,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -742,9 +743,9 @@ "output_type": "stream", "name": "stdout", "text": [ - " ID URI Created Assets Cache ID\n", - "---- ----------------------- ---------------- -------- ----------\n", - " 1 example_nbs/basic.ipynb 2020-03-13 14:21 0 1\n" + " ID URI Reader Added Status\n", + "---- ----------------------- -------- ---------------- --------\n", + " 1 example_nbs/basic.ipynb nbformat 2021-08-04 02:25 ✅ [1]\n" ] } ], @@ -766,7 +767,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "source": [ "cache.merge_match_into_file(\n", " cache.get_project_record(1).uri,\n", @@ -818,7 +819,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "source": [ "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic_failing.ipynb\"))\n", "record" @@ -839,7 +840,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "source": [ "cache.get_cached_project_nb(2) # returns None" ], @@ -848,7 +849,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "source": [ "cache.list_unexecuted()" ], @@ -868,7 +869,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -879,10 +880,10 @@ "output_type": "stream", "name": "stdout", "text": [ - " ID URI Created Assets Cache ID\n", - "---- ------------------------------- ---------------- -------- ----------\n", - " 2 example_nbs/basic_failing.ipynb 2020-03-13 14:21 0\n", - " 1 example_nbs/basic.ipynb 2020-03-13 14:21 0 1\n" + " ID URI Reader Added Status\n", + "---- ------------------------------- -------- ---------------- --------\n", + " 1 example_nbs/basic.ipynb nbformat 2021-08-04 02:25 ✅ [1]\n", + " 2 example_nbs/basic_failing.ipynb nbformat 2021-08-04 02:26 -\n" ] } ], @@ -897,7 +898,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "source": [ "cache.remove_nb_from_project(1)" ], @@ -906,7 +907,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -917,9 +918,9 @@ "output_type": "stream", "name": "stdout", "text": [ - " ID URI Created Assets\n", - "---- ------------------------------- ---------------- --------\n", - " 2 example_nbs/basic_failing.ipynb 2020-03-13 14:21 0\n" + " ID URI Reader Added Status\n", + "---- ------------------------------- -------- ---------------- --------\n", + " 2 example_nbs/basic_failing.ipynb nbformat 2021-08-04 02:26 -\n" ] } ], @@ -943,7 +944,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "source": [ "cache.clear_cache()\n", "cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", @@ -965,7 +966,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -976,10 +977,10 @@ "output_type": "stream", "name": "stdout", "text": [ - " ID URI Created Assets\n", - "---- ------------------------------- ---------------- --------\n", - " 2 example_nbs/basic_failing.ipynb 2020-03-13 14:21 0\n", - " 1 example_nbs/basic.ipynb 2020-03-13 14:21 0\n" + " ID URI Reader Added Status\n", + "---- ------------------------------- -------- ---------------- --------\n", + " 1 example_nbs/basic.ipynb nbformat 2021-08-04 02:26 -\n", + " 2 example_nbs/basic_failing.ipynb nbformat 2021-08-04 02:26 -\n" ] } ], @@ -999,7 +1000,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "source": [ "list_executors()" ], @@ -1008,7 +1009,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - "[EntryPoint.parse('basic = jupyter_cache.executors.basic:JupyterExecutorBasic')]" + "{'local-mproc', 'local-serial', 'temp-mproc', 'temp-serial'}" ] }, "metadata": {}, @@ -1019,12 +1020,12 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "source": [ "from logging import basicConfig, INFO\n", "basicConfig(level=INFO)\n", "\n", - "executor = load_executor(\"basic\", cache=cache)\n", + "executor = load_executor(\"local-serial\", cache=cache)\n", "executor" ], "outputs": [ @@ -1032,7 +1033,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - "JupyterExecutorBasic(cache=JupyterCacheBase('/Users/cjs14/GitHub/jupyter-cache/docs/using/.jupyter_cache'))" + "JupyterExecutorLocalSerial(cache=JupyterCacheBase('/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/.jupyter_cache'))" ] }, "metadata": {}, @@ -1066,7 +1067,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "source": [ "result = executor.run_and_cache()\n", "result" @@ -1076,19 +1077,31 @@ "output_type": "stream", "name": "stderr", "text": [ - "INFO:jupyter_cache.executors.base:Executing: /Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb\n", - "INFO:jupyter_cache.executors.base:Execution Succeeded: /Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb\n", - "INFO:jupyter_cache.executors.base:Executing: /Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic_failing.ipynb\n", - "ERROR:jupyter_cache.executors.base:Execution Failed: /Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic_failing.ipynb\n" + "INFO:jupyter_cache.executors.base:Executing 2 notebook(s) in serial\n", + "INFO:jupyter_cache.executors.base:Executing: /Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb\n", + "INFO:jupyter_cache.executors.base:Execution Successful: /Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb\n", + "INFO:jupyter_cache.executors.base:Executing: /Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic_failing.ipynb\n", + "WARNING:jupyter_cache.executors.base:Execution Excepted: /Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic_failing.ipynb\n", + "CellExecutionError: An error occurred while executing the following cell:\n", + "------------------\n", + "raise Exception('oopsie!')\n", + "------------------\n", + "\n", + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n", + "\u001b[0;31mException\u001b[0m Traceback (most recent call last)\n", + "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_80205/340246212.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", + "\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'oopsie!'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0m\n", + "\u001b[0;31mException\u001b[0m: oopsie!\n", + "Exception: oopsie!\n", + "\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ - "{'succeeded': ['/Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb'],\n", - " 'excepted': ['/Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic_failing.ipynb'],\n", - " 'errored': []}" + "ExecutorRunResult(succeeded=['/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb'], excepted=['/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic_failing.ipynb'], errored=[])" ] }, "metadata": {}, @@ -1106,7 +1119,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "source": [ "cache.list_cache_records()" ], @@ -1126,7 +1139,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "source": [ "record = cache.get_cache_record(1)\n", "record.to_dict()" @@ -1136,13 +1149,13 @@ "output_type": "execute_result", "data": { "text/plain": [ - "{'data': {'execution_seconds': 1.7455324890000004},\n", - " 'pk': 1,\n", - " 'uri': '/Users/cjs14/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb',\n", - " 'accessed': datetime.datetime(2020, 3, 13, 14, 21, 50, 803042),\n", + "{'pk': 1,\n", + " 'uri': '/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb',\n", + " 'data': {'execution_seconds': 1.5897068880003644},\n", + " 'accessed': datetime.datetime(2021, 8, 4, 2, 27, 39, 528685),\n", " 'description': '',\n", - " 'hashkey': '818f3412b998fcf4fe9ca3cca11a3fc3',\n", - " 'created': datetime.datetime(2020, 3, 13, 14, 21, 50, 803031)}" + " 'hashkey': '94c17138f782c75df59e989fffa64e3a',\n", + " 'created': datetime.datetime(2021, 8, 4, 2, 27, 39, 528680)}" ] }, "metadata": {}, @@ -1162,7 +1175,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "source": [ "record = cache.get_project_record(2)\n", "print(record.traceback)" @@ -1173,24 +1186,34 @@ "name": "stdout", "text": [ "Traceback (most recent call last):\n", - " File \"/Users/cjs14/GitHub/jupyter-cache/jupyter_cache/executors/basic.py\", line 152, in execute\n", - " executenb(nb_bundle.nb, cwd=tmpdirname)\n", - " File \"/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/execute.py\", line 737, in executenb\n", - " return ep.preprocess(nb, resources, km=km)[0]\n", - " File \"/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/execute.py\", line 405, in preprocess\n", - " nb, resources = super(ExecutePreprocessor, self).preprocess(nb, resources)\n", - " File \"/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/base.py\", line 69, in preprocess\n", - " nb.cells[index], resources = self.preprocess_cell(cell, resources, index)\n", - " File \"/anaconda/envs/mistune/lib/python3.7/site-packages/nbconvert/preprocessors/execute.py\", line 448, in preprocess_cell\n", - " raise CellExecutionError.from_cell_and_msg(cell, out)\n", - "nbconvert.preprocessors.execute.CellExecutionError: An error occurred while executing the following cell:\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/jupyter_cache/executors/utils.py\", line 55, in single_nb_execution\n", + " executenb(\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 1112, in execute\n", + " return NotebookClient(nb=nb, resources=resources, km=km, **kwargs).execute()\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/util.py\", line 74, in wrapped\n", + " return just_run(coro(*args, **kwargs))\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/util.py\", line 53, in just_run\n", + " return loop.run_until_complete(coro)\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nest_asyncio.py\", line 70, in run_until_complete\n", + " return f.result()\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/asyncio/futures.py\", line 178, in result\n", + " raise self._exception\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/asyncio/tasks.py\", line 280, in __step\n", + " result = coro.send(None)\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 553, in async_execute\n", + " await self.async_execute_cell(\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 857, in async_execute_cell\n", + " self._check_raise_for_error(cell, exec_reply)\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 760, in _check_raise_for_error\n", + " raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content)\n", + "nbclient.exceptions.CellExecutionError: An error occurred while executing the following cell:\n", "------------------\n", "raise Exception('oopsie!')\n", "------------------\n", "\n", "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n", "\u001b[0;31mException\u001b[0m Traceback (most recent call last)\n", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", + "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_80205/340246212.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", "\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'oopsie!'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0m\n", "\u001b[0;31mException\u001b[0m: oopsie!\n", @@ -1211,7 +1234,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 41, "source": [ "print(tabulate_project_records(\n", " cache.list_project_records(), path_length=2, cache=cache\n", @@ -1222,10 +1245,10 @@ "output_type": "stream", "name": "stdout", "text": [ - " ID URI Created Assets Cache ID\n", - "---- ------------------------------- ---------------- -------- ----------\n", - " 2 example_nbs/basic_failing.ipynb 2020-03-13 14:21 0\n", - " 1 example_nbs/basic.ipynb 2020-03-13 14:21 0 1\n" + " ID URI Reader Added Status\n", + "---- ------------------------------- -------- ---------------- --------\n", + " 1 example_nbs/basic.ipynb nbformat 2021-08-04 02:26 ✅ [1]\n", + " 2 example_nbs/basic_failing.ipynb nbformat 2021-08-04 02:26 ❌\n" ] } ], @@ -1233,7 +1256,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 42, "source": [ "print(tabulate_cache_records(\n", " cache.list_cache_records(), path_length=1, hashkeys=True\n", @@ -1246,7 +1269,7 @@ "text": [ " ID Origin URI Created Accessed Hashkey\n", "---- ------------ ---------------- ---------------- --------------------------------\n", - " 1 basic.ipynb 2020-03-13 14:21 2020-03-13 14:21 818f3412b998fcf4fe9ca3cca11a3fc3\n" + " 1 basic.ipynb 2021-08-04 02:27 2021-08-04 02:27 94c17138f782c75df59e989fffa64e3a\n" ] } ], diff --git a/docs/using/cli.md b/docs/using/cli.md index e19cc9b..1b4ae1f 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -104,7 +104,7 @@ Note artefact paths must be 'upstream' of the notebook folder: To view the contents of an execution artefact: ```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: cat-artifact +:command: cat-artefact :args: 6 artifact_folder/artifact.txt ``` diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index 670295f..92aadf9 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -37,6 +37,30 @@ def __init__(self, message, nb_bundle, *args, **kwargs): super().__init__(message, *args, **kwargs) +@attr.s(frozen=True, slots=True) +class ProjectNb: + """A notebook read from a project""" + + pk: int = attr.ib( + validator=instance_of(int), + metadata={"help": "the ID of the notebook"}, + ) + uri: str = attr.ib( + converter=str, + validator=instance_of(str), + metadata={"help": "the URI of the notebook"}, + ) + nb: nbf.NotebookNode = attr.ib( + validator=instance_of(nbf.NotebookNode), + repr=lambda nb: "Notebook(cells={0})".format(len(nb.cells)), + metadata={"help": "the notebook"}, + ) + assets: List[Path] = attr.ib( + factory=list, + metadata={"help": "File paths required to run the notebook"}, + ) + + class NbArtifactsAbstract(ABC): """Container for artefacts of a notebook execution.""" @@ -56,7 +80,7 @@ def __repr__(self): @attr.s(frozen=True, slots=True) -class NbBundleIn: +class CacheBundleIn: """A container for notebooks and their associated data to cache.""" nb: nbf.NotebookNode = attr.ib( @@ -89,7 +113,7 @@ class NbBundleIn: @attr.s(frozen=True, slots=True) -class NbBundleOut: +class CacheBundleOut: """A container for notebooks and their associated data that have been cached.""" nb: nbf.NotebookNode = attr.ib( @@ -105,7 +129,10 @@ class NbBundleOut: class JupyterCacheAbstract(ABC): - """An abstract cache for storing pre/post executed notebooks.""" + """An abstract cache for storing pre/post executed notebooks. + + Note: class instances should be pickleable. + """ @abstractmethod def clear_cache(self) -> None: @@ -113,7 +140,10 @@ def clear_cache(self) -> None: @abstractmethod def cache_notebook_bundle( - self, bundle: NbBundleIn, check_validity: bool = True, overwrite: bool = False + self, + bundle: CacheBundleIn, + check_validity: bool = True, + overwrite: bool = False, ) -> NbCacheRecord: """Commit an executed notebook, returning its cache record. @@ -160,7 +190,7 @@ def get_cache_record(self, pk: int) -> NbCacheRecord: """Return the record of a cache, by its primary key""" @abstractmethod - def get_cache_bundle(self, pk: int) -> NbBundleOut: + def get_cache_bundle(self, pk: int) -> CacheBundleOut: """Return an executed notebook bundle, by its primary key""" @abstractmethod @@ -266,7 +296,7 @@ def get_project_record(self, uri_or_pk: Union[int, str]) -> NbProjectRecord: """Return the record of a notebook in the project, by its primary key or URI.""" @abstractmethod - def get_project_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: + def get_project_notebook(self, uri_or_pk: Union[int, str]) -> ProjectNb: """Return a single notebook in the project, by its primary key or URI.""" @abstractmethod diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 0bebc10..c8d3725 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -79,7 +79,6 @@ def get_value(key: str, db: Engine, default=None): result = session.query(Setting.value).filter_by(key=key).one_or_none() if result is None: if default is not None: - Setting.set_value(key, default, db) result = [default] else: raise KeyError("Setting not found in DB: {}".format(key)) diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index d4845ec..482ca98 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -10,12 +10,13 @@ from jupyter_cache.base import ( # noqa: F401 NB_VERSION, + CacheBundleIn, + CacheBundleOut, CachingError, JupyterCacheAbstract, NbArtifactsAbstract, - NbBundleIn, - NbBundleOut, NbValidityError, + ProjectNb, RetrievalError, ) from jupyter_cache.readers import get_reader @@ -172,7 +173,7 @@ def create_hashed_notebook( return (nb, hash_string) - def _validate_nb_bundle(self, nb_bundle: NbBundleIn): + def _validate_nb_bundle(self, nb_bundle: CacheBundleIn): """Validate that a notebook bundle should be cached. We check that the notebook has been executed correctly, @@ -195,7 +196,7 @@ def _validate_nb_bundle(self, nb_bundle: NbBundleIn): def cache_notebook_bundle( self, - bundle: NbBundleIn, + bundle: CacheBundleIn, check_validity: bool = True, overwrite: bool = False, description="", @@ -267,7 +268,7 @@ def cache_notebook_file( """ notebook = nbf.read(str(path), nbf.NO_CONVERT) return self.cache_notebook_bundle( - NbBundleIn( + CacheBundleIn( notebook, uri or str(path), artifacts=NbArtifacts(artifacts, in_folder=Path(path).parent), @@ -283,7 +284,7 @@ def list_cache_records(self) -> List[NbCacheRecord]: def get_cache_record(self, pk: int) -> NbCacheRecord: return NbCacheRecord.record_from_pk(pk, self.db) - def get_cache_bundle(self, pk: int) -> NbBundleOut: + def get_cache_bundle(self, pk: int) -> CacheBundleOut: record = NbCacheRecord.record_from_pk(pk, self.db) NbCacheRecord.touch(pk, self.db) path = self._get_notebook_path_cache(record.hashkey) @@ -293,7 +294,7 @@ def get_cache_bundle(self, pk: int) -> NbBundleOut: "Notebook file does not exist for cache record PK: {}".format(pk) ) - return NbBundleOut( + return CacheBundleOut( nbf.reads(path.read_text(encoding="utf8"), nbf.NO_CONVERT), record=record, artifacts=NbArtifacts( @@ -433,7 +434,7 @@ def remove_nb_from_project(self, uri_or_pk: Union[int, str]): # TODO add discard all/multiple project records method - def get_project_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: + def get_project_notebook(self, uri_or_pk: Union[int, str]) -> ProjectNb: if isinstance(uri_or_pk, int): record = NbProjectRecord.record_from_pk(uri_or_pk, self.db) else: @@ -444,7 +445,7 @@ def get_project_notebook(self, uri_or_pk: Union[int, str]) -> NbBundleIn: ) converter = get_reader(record.reader) notebook = converter(record.uri) - return NbBundleIn(notebook, record.uri) + return ProjectNb(record.pk, record.uri, notebook, record.assets) def get_cached_project_nb( self, uri_or_pk: Union[int, str] diff --git a/jupyter_cache/cli/commands/cmd_cache.py b/jupyter_cache/cli/commands/cmd_cache.py index ceef878..290e501 100644 --- a/jupyter_cache/cli/commands/cmd_cache.py +++ b/jupyter_cache/cli/commands/cmd_cache.py @@ -71,7 +71,7 @@ def show_cache(cache_path, pk): click.echo(f"- {path}") -@cmnd_cache.command("cat-artifact") +@cmnd_cache.command("cat-artefact") @options.CACHE_PATH @arguments.PK @arguments.ARTIFACT_RPATH diff --git a/jupyter_cache/cli/commands/cmd_project.py b/jupyter_cache/cli/commands/cmd_project.py index 2e84a22..7e98fe8 100644 --- a/jupyter_cache/cli/commands/cmd_project.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -134,8 +134,8 @@ def show_project_record(cache_path, pk, tb): def merge_executed(cache_path, pk_path, outpath): """Write notebook merged with cached outputs (by ID/URI).""" db = get_cache(cache_path) - bundle = db.get_project_notebook(int(pk_path) if pk_path.isdigit() else pk_path) - cached_pk, nb = db.merge_match_into_notebook(bundle.nb) + nb = db.get_project_notebook(int(pk_path) if pk_path.isdigit() else pk_path).nb + cached_pk, nb = db.merge_match_into_notebook(nb) nbformat.write(nb, outpath) click.echo(f"Merged with cache PK {cached_pk}") click.secho("Success!", fg="green") diff --git a/jupyter_cache/executors/base.py b/jupyter_cache/executors/base.py index 869a542..3f2115e 100644 --- a/jupyter_cache/executors/base.py +++ b/jupyter_cache/executors/base.py @@ -5,6 +5,7 @@ import attr from jupyter_cache.base import JupyterCacheAbstract +from jupyter_cache.cache.db import NbProjectRecord from jupyter_cache.entry_points import ( ENTRY_POINT_GROUP_EXEC, get_entry_point, @@ -60,6 +61,27 @@ def cache(self): def logger(self): return self._logger + def get_records( + self, + filter_uris: Optional[List[str]] = None, + filter_pks: Optional[List[int]] = None, + clear_tracebacks: bool = True, + ) -> List[NbProjectRecord]: + """Return records to execute. + + :param clear_tracebacks: Remove any tracebacks from previous executions + """ + execute_records = self.cache.list_unexecuted() + if filter_uris is not None: + execute_records = [r for r in execute_records if r.uri in filter_uris] + if filter_pks is not None: + execute_records = [r for r in execute_records if r.pk in filter_pks] + if clear_tracebacks: + NbProjectRecord.remove_tracebacks( + [r.pk for r in execute_records], self.cache.db + ) + return execute_records + @abstractmethod def run_and_cache( self, diff --git a/jupyter_cache/executors/basic.py b/jupyter_cache/executors/basic.py index 32fbb76..4eec72c 100644 --- a/jupyter_cache/executors/basic.py +++ b/jupyter_cache/executors/basic.py @@ -1,34 +1,195 @@ -import shutil +import logging +import multiprocessing as mproc +import os import tempfile from pathlib import Path -from typing import Optional +from typing import NamedTuple, Tuple +from jupyter_cache.base import JupyterCacheAbstract, ProjectNb from jupyter_cache.cache.db import NbProjectRecord -from jupyter_cache.cache.main import NbArtifacts, NbBundleIn from jupyter_cache.executors.base import ExecutorRunResult, JupyterExecutorAbstract -from jupyter_cache.executors.utils import single_nb_execution -from jupyter_cache.utils import to_relative_paths +from jupyter_cache.executors.utils import ( + ExecutionResult, + copy_assets, + create_cache_bundle, + single_nb_execution, +) -# from jupyter_client.kernelspec import get_kernel_spec, NoSuchKernel +REPORT_LEVEL = logging.INFO + 1 +logging.addLevelName(REPORT_LEVEL, "REPORT") -class ExecutionError(Exception): - """An exception to signify a error during execution of a specific URI.""" +class ProcessData(NamedTuple): + """Data for the process worker.""" - def __init__(self, message, uri, exc): - self.uri = uri - self.exc = exc - return super().__init__(message) + pk: int + uri: str + cache: JupyterCacheAbstract + timeout: int + allow_errors: bool -class JupyterExecutorLocalSerial(JupyterExecutorAbstract): - """A basic implementation of an executor; executing locally in serial. +class ExecutionWorkerBase: + """Base execution worker. - The execution is split into two methods: `run` and `execute`. - In this way access to the cache can be synchronous, but the execution can be - multi/async processed. Takes timeout parameter in seconds for execution + Note this must be pickleable. """ + @property + def logger(self) -> logging.Logger: + raise NotImplementedError + + def log_info(self, msg: str): + self.logger.info(msg) + + def execute(self, project_nb: ProjectNb, data: ProcessData) -> ExecutionResult: + raise NotImplementedError + + def __call__(self, data: ProcessData) -> Tuple[int, str]: + + try: + project_nb = data.cache.get_project_notebook(data.pk) + except Exception: + self.logger.error( + "Failed Retrieving: %s" % data.uri, + exc_info=True, + ) + return (2, data.uri) + + try: + self.log_info("Executing: %s" % project_nb.uri) + result = self.execute(project_nb, data) + except Exception: + self.logger.error( + "Failed Executing: %s" % data.uri, + exc_info=True, + ) + return (2, data.uri) + + if result.err: + self.logger.warning( + "Execution Excepted: %s\n%s: %s" + % (project_nb.uri, type(result.err).__name__, str(result.err)) + ) + NbProjectRecord.set_traceback( + project_nb.uri, result.exc_string, data.cache.db + ) + return (1, data.uri) + + self.log_info("Execution Successful: %s" % project_nb.uri) + try: + # TODO deal with artifact retrieval + bundle = create_cache_bundle( + project_nb, result.cwd, None, result.time, result.exc_string + ) + data.cache.cache_notebook_bundle( + bundle, check_validity=False, overwrite=True + ) + except Exception: + self.logger.error( + "Failed Caching: %s" % data.uri, + exc_info=True, + ) + return (2, data.uri) + + return (0, data.uri) + + +class ExecutionWorkerLocalSerial(ExecutionWorkerBase): + """Execution worker, that executes in local folder.""" + + def __init__(self, logger: logging.Logger) -> None: + super().__init__() + self._logger = logger + + @property + def logger(self) -> logging.Logger: + return self._logger + + @staticmethod + def execute(project_nb: ProjectNb, data: ProcessData) -> ExecutionResult: + cwd = str(Path(project_nb.uri).parent) + return single_nb_execution( + project_nb.nb, + cwd=cwd, + timeout=data.timeout, + allow_errors=data.allow_errors, + ) + + +class ExecutionWorkerTempSerial(ExecutionWorkerBase): + """Execution worker, that executes in temporary folder.""" + + def __init__(self, logger: logging.Logger) -> None: + super().__init__() + self._logger = logger + + @property + def logger(self) -> logging.Logger: + return self._logger + + @staticmethod + def execute(project_nb: ProjectNb, data: ProcessData) -> ExecutionResult: + with tempfile.TemporaryDirectory() as cwd: + copy_assets(project_nb.uri, project_nb.assets, cwd) + return single_nb_execution( + project_nb.nb, + cwd=cwd, + timeout=data.timeout, + allow_errors=data.allow_errors, + ) + + +class ExecutionWorkerLocalMProc(ExecutionWorkerBase): + """Execution worker, that executes in local folder.""" + + @property + def logger(self) -> logging.Logger: + return mproc.get_logger() + + def log_info(self, msg: str): + # multiprocessing logs a lot at info level that we do not want to see + self.logger.log(REPORT_LEVEL, msg) + + @staticmethod + def execute(project_nb: ProjectNb, data: ProcessData) -> ExecutionResult: + cwd = str(Path(project_nb.uri).parent) + return single_nb_execution( + project_nb.nb, + cwd=cwd, + timeout=data.timeout, + allow_errors=data.allow_errors, + ) + + +class ExecutionWorkerTempMProc(ExecutionWorkerBase): + """Execution worker, that executes in temporary folder.""" + + @property + def logger(self) -> logging.Logger: + return mproc.get_logger() + + def log_info(self, msg: str): + # multiprocessing logs a lot at info level that we do not want to see + self.logger.log(REPORT_LEVEL, msg) + + @staticmethod + def execute(project_nb: ProjectNb, data: ProcessData) -> ExecutionResult: + with tempfile.TemporaryDirectory() as cwd: + copy_assets(project_nb.uri, project_nb.assets, cwd) + return single_nb_execution( + project_nb.nb, + cwd=cwd, + timeout=data.timeout, + allow_errors=data.allow_errors, + ) + + +class JupyterExecutorLocalSerial(JupyterExecutorAbstract): + """An implementation of an executor; executing locally in serial.""" + + _EXECUTION_WORKER = ExecutionWorkerLocalSerial + def run_and_cache( self, *, @@ -37,192 +198,77 @@ def run_and_cache( timeout=30, allow_errors=False, ) -> ExecutorRunResult: - """This function interfaces with the cache, deferring execution to `execute`.""" - # Get the notebook tha require re-execution - execute_records = self.cache.list_unexecuted() - if filter_uris is not None: - execute_records = [r for r in execute_records if r.uri in filter_uris] - if filter_pks is not None: - execute_records = [r for r in execute_records if r.pk in filter_pks] - - # remove any tracebacks from previous executions - NbProjectRecord.remove_tracebacks( - [r.pk for r in execute_records], self.cache.db + # Get the notebook that require re-execution + execute_records = self.get_records( + filter_uris, filter_pks, clear_tracebacks=True ) - # setup an dictionary to categorise all executed notebook uris: - # excepted are where the actual notebook execution raised an exception; - # errored is where any other exception was raised - result = ExecutorRunResult() - # we pass an iterator to the execute method, - # so that we don't have to read all notebooks before execution - - def _iterator(): - for execute_record in execute_records: - try: - nb_bundle = self.cache.get_project_notebook(execute_record.pk) - except Exception: - self.logger.error( - "Failed Retrieving: {}".format(execute_record.uri), - exc_info=True, - ) - result.errored.append(execute_record.uri) - else: - yield execute_record, nb_bundle - - # The execute method yields notebook bundles, or ExecutionError - for bundle_or_exc in self.execute( - _iterator(), - int(timeout), - allow_errors, - ): - if isinstance(bundle_or_exc, ExecutionError): - self.logger.error(bundle_or_exc.uri, exc_info=bundle_or_exc.exc) - result.errored.append(bundle_or_exc.uri) - continue - elif bundle_or_exc.traceback is not None: - # The notebook raised an exception during execution - # TODO store excepted bundles - result.excepted.append(bundle_or_exc.uri) - NbProjectRecord.set_traceback( - bundle_or_exc.uri, bundle_or_exc.traceback, self.cache.db - ) - continue - try: - # cache a successfully executed notebook - self.cache.cache_notebook_bundle( - bundle_or_exc, check_validity=False, overwrite=True - ) - except Exception: - self.logger.error( - "Failed Caching: {}".format(bundle_or_exc.uri), exc_info=True - ) - result.errored.append(bundle_or_exc.uri) - else: - result.succeeded.append(bundle_or_exc.uri) - - # TODO it would also be ideal to tag all notebooks - # that were executed at the same time (just part of `data` or separate column?). - # TODO maybe the status of success/failure could be explicitly stored on - # the project record (cache_status=Enum('OK', 'FAILED', 'MISSING')) - # although now traceback is so this is an implicit sign of failure, - # TODO failed notebooks could be stored in the cache, which would be - # accessed by the project pk (and would be deleted when removing the project record) - # see: https://python.quantecon.org/status.html - - return result - - def execute_single( - self, - nb_bundle, - uri: str, - cwd: Optional[str], - timeout: Optional[int], - allow_errors: bool, - asset_files, - ): - result = single_nb_execution( - nb_bundle.nb, - cwd=cwd, - timeout=timeout, - allow_errors=allow_errors, - ) - if result.err: - self.logger.error("Execution Failed: {}".format(uri)) - return _create_bundle( - nb_bundle, - cwd, - asset_files, - result.time, - result.exc_string, + self.logger.info("Executing %s notebook(s) in serial" % len(execute_records)) + + results = [ + self._EXECUTION_WORKER(self.logger)( + ProcessData(record.pk, record.uri, self.cache, timeout, allow_errors) ) + for record in execute_records + ] - self.logger.info("Execution Succeeded: {}".format(uri)) - return _create_bundle(nb_bundle, cwd, asset_files, result.time, None) + return ExecutorRunResult( + succeeded=[p for i, p in results if i == 0], + excepted=[p for i, p in results if i == 1], + errored=[p for i, p in results if i == 2], + ) - def execute(self, input_iterator, timeout=30, allow_errors=False): - """This function is isolated from the cache, and is responsible for execution. - The method is only supplied with the project record and input notebook bundle, - it then yields results for caching - """ - for _, nb_bundle in input_iterator: - try: - uri = nb_bundle.uri - self.logger.info("Executing: {}".format(uri)) +class JupyterExecutorTempSerial(JupyterExecutorLocalSerial): + """An implementation of an executor; executing in a temporary folder in serial.""" - yield self.execute_single( - nb_bundle, - uri, - str(Path(uri).parent), - timeout, - allow_errors, - None, - ) + _EXECUTION_WORKER = ExecutionWorkerTempSerial - except Exception as err: - yield ExecutionError("Unexpected Error", uri, err) +class JupyterExecutorLocalMproc(JupyterExecutorAbstract): + """An implementation of an executor; executing locally in parallel.""" -class JupyterExecutorTempSerial(JupyterExecutorLocalSerial): - """An implementation of an executor; executing in a temporary folder in serial.""" + _EXECUTION_WORKER = ExecutionWorkerLocalMProc - def execute(self, input_iterator, timeout=30, allow_errors=False): - """This function is isolated from the cache, and is responsible for execution. - - The method is only supplied with the project record and input notebook bundle, - it then yields results for caching. - """ - for execute_record, nb_bundle in input_iterator: - try: - uri = nb_bundle.uri - self.logger.info("Executing: {}".format(uri)) - - with tempfile.TemporaryDirectory() as tmpdirname: - - try: - asset_files = _copy_assets(execute_record, tmpdirname) - except Exception as err: - yield ExecutionError("Assets Retrieval Error", uri, err) - continue - - yield self.execute_single( - nb_bundle, - uri, - tmpdirname, - timeout, - allow_errors, - asset_files, - ) + def run_and_cache( + self, + *, + filter_uris=None, + filter_pks=None, + timeout=30, + allow_errors=False, + ) -> ExecutorRunResult: + # Get the notebook that require re-execution + execute_records = self.get_records( + filter_uris, filter_pks, clear_tracebacks=True + ) - except Exception as err: - yield ExecutionError("Unexpected Error", uri, err) - - -def _copy_assets(record, folder): - """Copy notebook assets to the folder the notebook will be executed in.""" - asset_files = [] - relative_paths = to_relative_paths(record.assets, Path(record.uri).parent) - for path, rel_path in zip(record.assets, relative_paths): - temp_file = Path(folder).joinpath(rel_path) - temp_file.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(path, temp_file) - asset_files.append(temp_file) - return asset_files - - -def _create_bundle(nb_bundle, tmpdirname, asset_files, exec_time, exec_tb): - """Create a cache bundle.""" - return NbBundleIn( - nb_bundle.nb, - nb_bundle.uri, - # TODO retrieve assets that have changed file mtime? - artifacts=NbArtifacts( - [p for p in Path(tmpdirname).glob("**/*") if p not in asset_files], - tmpdirname, + self.logger.info( + "Executing %s notebook(s) over pool of %s processors" + % (len(execute_records), os.cpu_count()) + ) + mproc.log_to_stderr( + REPORT_LEVEL if self.logger.level == logging.INFO else self.logger.level ) - if asset_files is not None - else None, - data={"execution_seconds": exec_time}, - traceback=exec_tb, - ) + + with mproc.Pool() as pool: + results = pool.map( + self._EXECUTION_WORKER(), + [ + ProcessData( + record.pk, record.uri, self.cache, timeout, allow_errors + ) + for record in execute_records + ], + ) + return ExecutorRunResult( + succeeded=[p for i, p in results if i == 0], + excepted=[p for i, p in results if i == 1], + errored=[p for i, p in results if i == 2], + ) + + +class JupyterExecutorTempMproc(JupyterExecutorLocalMproc): + """An implementation of an executor; executing in a temporary directory and in parallel.""" + + _EXECUTION_WORKER = ExecutionWorkerTempMProc diff --git a/jupyter_cache/executors/utils.py b/jupyter_cache/executors/utils.py index 01b90b0..8a8a91b 100644 --- a/jupyter_cache/executors/utils.py +++ b/jupyter_cache/executors/utils.py @@ -1,17 +1,22 @@ +import shutil import traceback -from typing import Optional, Union +from pathlib import Path +from typing import List, Optional, Union import attr from nbclient import execute as executenb from nbclient.client import CellExecutionError, CellTimeoutError from nbformat import NotebookNode -from jupyter_cache.utils import Timer +from jupyter_cache.base import CacheBundleIn, ProjectNb +from jupyter_cache.cache.main import NbArtifacts +from jupyter_cache.utils import Timer, to_relative_paths @attr.s() class ExecutionResult: nb: NotebookNode = attr.ib() + cwd: str = attr.ib() time: float = attr.ib() err: Optional[Union[CellExecutionError, CellTimeoutError]] = attr.ib(default=None) exc_string: Optional[str] = attr.ib(default=None) @@ -58,4 +63,39 @@ def single_nb_execution( error = err exc_string = "".join(traceback.format_exc()) - return ExecutionResult(nb, timer.last_split, error, exc_string) + return ExecutionResult(nb, cwd, timer.last_split, error, exc_string) + + +def copy_assets(uri: str, assets: List[str], folder: str) -> List[Path]: + """Copy notebook assets to the folder the notebook will be executed in.""" + asset_files = [] + relative_paths = to_relative_paths(assets, Path(uri).parent) + for path, rel_path in zip(assets, relative_paths): + temp_file = Path(folder).joinpath(rel_path) + temp_file.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(path, temp_file) + asset_files.append(temp_file) + return asset_files + + +def create_cache_bundle( + project_nb: ProjectNb, + execdir: Optional[str], + asset_files: Optional[List[Path]], + exec_time: float, + exec_tb: Optional[str], +) -> CacheBundleIn: + """Create a cache bundle to save.""" + return CacheBundleIn( + project_nb.nb, + project_nb.uri, + # TODO retrieve assets that have changed file mtime? + artifacts=NbArtifacts( + [p for p in Path(execdir).glob("**/*") if p not in asset_files], + execdir, + ) + if (execdir is not None and asset_files is not None) + else None, + data={"execution_seconds": exec_time}, + traceback=exec_tb, + ) diff --git a/setup.cfg b/setup.cfg index 482a764..ed55cfc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,8 @@ console_scripts = jcache.executors = local-serial = jupyter_cache.executors.basic:JupyterExecutorLocalSerial temp-serial = jupyter_cache.executors.basic:JupyterExecutorTempSerial + local-mproc = jupyter_cache.executors.basic:JupyterExecutorLocalMproc + temp-mproc = jupyter_cache.executors.basic:JupyterExecutorTempMproc jcache.readers = nbformat = jupyter_cache.readers:nbf_reader jupytext = jupyter_cache.readers:jupytext_reader diff --git a/tests/test_cache.py b/tests/test_cache.py index 12db3f4..41c6924 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -182,7 +182,9 @@ def test_artifacts(tmp_path): assert path.joinpath("artifact_folder").exists() -@pytest.mark.parametrize("executor_key", ["local-serial", "temp-serial"]) +@pytest.mark.parametrize( + "executor_key", ["local-serial", "temp-serial", "local-mproc", "temp-mproc"] +) def test_execution(tmp_path, executor_key): from jupyter_cache.executors import load_executor @@ -197,8 +199,10 @@ def test_execution(tmp_path, executor_key): ) executor = load_executor(executor_key, db) result = executor.run_and_cache() - print(result) - assert result.as_json() == { + # print(result) + json_result = result.as_json() + json_result["succeeded"] = list(sorted(json_result.get("succeeded", []))) + assert json_result == { "succeeded": [ os.path.join(temp_nb_path, "basic_unrun.ipynb"), os.path.join(temp_nb_path, "external_output.ipynb"), @@ -207,7 +211,8 @@ def test_execution(tmp_path, executor_key): "errored": [], } assert len(db.list_cache_records()) == 2 - bundle = db.get_cache_bundle(1) + cache_record = db.get_cached_project_nb(1) + bundle = db.get_cache_bundle(cache_record.pk) assert bundle.nb.cells[0] == { "cell_type": "code", "execution_count": 1, @@ -216,11 +221,13 @@ def test_execution(tmp_path, executor_key): "source": "a=1\nprint(a)", } assert "execution_seconds" in bundle.record.data - if "temp" in executor_key: - with db.cache_artefacts_temppath(2) as path: - paths = [str(p.relative_to(path)) for p in path.glob("**/*") if p.is_file()] - assert paths == ["artifact.txt"] - assert path.joinpath("artifact.txt").read_text(encoding="utf8") == "hi" + + # TODO artifacts + # with db.cache_artefacts_temppath(2) as path: + # paths = [str(p.relative_to(path)) for p in path.glob("**/*") if p.is_file()] + # assert paths == ["artifact.txt"] + # assert path.joinpath("artifact.txt").read_text(encoding="utf8") == "hi" + project_record = db.get_project_record(2) assert project_record.traceback is not None assert "Exception: oopsie!" in project_record.traceback From 0a5d1fe3e5b3b2467ea4daf2e7d5e4ffc2b08ac6 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 4 Aug 2021 04:59:38 +0200 Subject: [PATCH 14/39] =?UTF-8?q?=F0=9F=94=A7=20MAINTAIN:=20Re-add=20pytho?= =?UTF-8?q?n=203.6=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yml | 2 +- codecov.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index af17ff8..03ca95a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9] include: - os: windows-latest python-version: 3.7 diff --git a/codecov.yml b/codecov.yml index cf28904..01c8765 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,5 +6,5 @@ coverage: threshold: 0.5% patch: default: - target: 80% + target: 75% threshold: 0.5% From 6ee0f7930d6bf623feef97809092e95beb8673fa Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 4 Aug 2021 08:13:21 +0200 Subject: [PATCH 15/39] =?UTF-8?q?=F0=9F=93=9A=20DOCS:=20Re-write=20CLI=20t?= =?UTF-8?q?utorial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conf.py | 9 +- docs/index.md | 5 + docs/using/api.ipynb | 2 +- docs/using/cli.md | 246 ++++++++++++---------- jupyter_cache/cache/db.py | 8 +- jupyter_cache/cli/commands/cmd_project.py | 19 +- setup.cfg | 5 +- tests/test_cache.py | 2 +- 8 files changed, 175 insertions(+), 121 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2f6cebf..c0f0762 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -158,14 +158,15 @@ def run(self): finally: os.chdir(old_cwd) - if result.exit_code != 0 and "allow-exception" not in self.options: - raise self.error( - f"CLI non-zero exit code: {result.exit_code}\n---\n{result.output}\n---\n" - ) if result.exception and "allow-exception" not in self.options: raise self.error( f"CLI raised exception: {result.exception}\n---\n{result.output}\n---\n" ) + if result.exit_code != 0 and "allow-exception" not in self.options: + raise self.error( + f"CLI non-zero exit code: {result.exit_code}\n---\n{result.output}\n---\n" + ) + text = f"$ {' '.join(cmd_string)} {args}\n{result.output}" text = text.replace(root_path + os.sep, "../") node = nodes.literal_block(text, text, language="console") diff --git a/docs/index.md b/docs/index.md index 4b2b510..5d11509 100644 --- a/docs/index.md +++ b/docs/index.md @@ -100,7 +100,12 @@ Some desired requirements (not yet all implemented): ## Contents ```{toctree} +:caption: Tutorials using/cli using/api +``` + +```{toctree} +:caption: Development develop/contributing ``` diff --git a/docs/using/api.ipynb b/docs/using/api.ipynb index 8311b9d..858bd54 100644 --- a/docs/using/api.ipynb +++ b/docs/using/api.ipynb @@ -1009,7 +1009,7 @@ "output_type": "execute_result", "data": { "text/plain": [ - "{'local-mproc', 'local-serial', 'temp-mproc', 'temp-serial'}" + "{'local-parallel', 'local-serial', 'temp-parallel', 'temp-serial'}" ] }, "metadata": {}, diff --git a/docs/using/cli.md b/docs/using/cli.md index 1b4ae1f..2afe2c8 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -5,7 +5,7 @@ ```{jcache-clear} ``` -From the checked-out repository folder: +Note, you can follow this tutorial from the checked-out repository folder: ```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache :args: --help @@ -18,6 +18,18 @@ The first time the cache is required, it will be lazily created: :input: y ``` +You can also clear it at any time: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +:command: clear +:input: y +``` + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: list +:input: y +``` + ````{tip} Execute this in the terminal for auto-completion: @@ -26,188 +38,210 @@ eval "$(_JCACHE_COMPLETE=source jcache)" ``` ```` -## Caching Executed Notebooks +## Adding notebooks to the project -```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :args: --help ``` -Initially there will be no cached notebooks: +A project consist of a set of notebooks to be executed. -```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +When adding notebooks to the project, they are recorded by their URI (e.g. file path), +i.e. no physical copying takes place until execution time. + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: add +:args: tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb +``` + +You can list the notebooks in the project, at present none have an existing execution record in the cache: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: list ``` -You can add notebooks straight into the cache. -When caching, a check will be made that the notebooks look to have been executed -correctly, i.e. the cell execution counts go sequentially up from 1. +You can remove a notebook from the project by its URI or ID: -```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: add -:args: tests/notebooks/basic.ipynb +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: remove +:args: 4 +``` + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: list +``` + +or clear all notebooks from the project: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: clear :input: y ``` -Or to skip validation: +By default, notebook files are read using the [nbformat reader](https://nbformat.readthedocs.io/en/latest/api.html#nbformat.read). +However, you can also specify a custom reader, defined by an entry point in the `jcache.readers` group. +Included with jupyter_cache is the [jupytext](https://jupytext.readthedocs.io) reader, for formats like MyST Markdown: -```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: add -:args: --no-validate tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb +:args: --reader nbformat tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb ``` -Once you've cached some notebooks, you can look at the 'cache records' -for what has been cached. - -Each notebook is hashed (code cells and kernel spec only), -which is used to compare against notebooks in the project. -Multiple hashes for the same URI can be added -(the URI is just there for inspection) and the size of the cache is limited -(current default 1000) so that, at this size, -the last accessed records begin to be deleted. -You can remove cached records by their ID. +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: add +:args: --reader jupytext tests/notebooks/basic.md +``` -```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: list ``` -````{tip} -To only show the latest versions of cached notebooks. +:::{important} +To use the `jupytext` reader, you must have the `jupytext` package installed. +::: -```console -$ jcache cache list --latest-only -``` -```` +## Executing the notebooks -You can also cache notebooks with artefacts -(external outputs of the notebook execution). +Simply call the `execute` command, to execute all notebooks in the project that do not have an existing record in the cache. -```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: add-with-artefacts -:args: --no-validate -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt -:input: y +Executors are defined by entry points in the `jcache.executors` group. +jupyter-cache includes these executors: + +- `local-serial`: execute notebooks with the working directory set to their path, in serial mode (using a single process). +- `local-parallel`: execute notebooks with the working directory set to their path, in parallel mode (using multiple processes). +- `temp-serial`: execute notebooks with a temporary working directory, in serial mode (using a single process). +- `temp-parallel`: execute notebooks with a temporary working directory, in parallel mode (using multiple processes). + +```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +:command: execute +:args: --executor local-serial ``` -Show a full description of a cached notebook by referring to its ID +Successfully executed notebooks will now have a record in the cache, uniquely identified by the a hash of their code and metadata content: ```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: show -:args: 6 +:command: list +:args: --hashkeys ``` -Note artefact paths must be 'upstream' of the notebook folder: +These records are then compared to the hashes of notebooks in the project, to find which have up-to-date executions. +Note here both notebooks share the same cached notebook (denoted by `[1]` in the status): -```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: add-with-artefacts -:args: -nb tests/notebooks/basic.ipynb tests/test_db.py +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: list ``` -To view the contents of an execution artefact: +Next time you execute the project, only notebooks which don't match a cached record will be executed: -```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: cat-artefact -:args: 6 artifact_folder/artifact.txt +```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +:command: execute +:args: --executor local-serial -v CRITICAL ``` -You can directly remove a cached notebook by its ID: +If you modify a code cell, the notebook will no longer match a cached notebook or, if you wish to re-execute unchanged notebook(s) (for example if the runtime environment has changed), you can remove their records from the cache (keeping the project record): ```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: remove -:args: 4 +:command: clear +:input: n +:allow-exception: ``` -You can also diff any of the cached notebooks with any (external) notebook: +:::{note} +The number of notebooks in the cache is limited +(current default 1000). +Once this limit is reached, the oldest (last accessed) notebooks begin to be deleted. +change this default with `jcache config cache-limit` +::: + +## Analysing executed/excepted notebooks + +You can see the elapsed execution time of a notebook via its ID in the cache: ```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: diff-nb -:args: 2 tests/notebooks/basic.ipynb +:command: show +:args: 1 ``` -## Adding notebooks to the project +Failed execution tracebacks are also available on the project record: ```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project -:args: --help +:command: show +:args: --tb tests/notebooks/basic_failing.ipynb ``` -A project consist of a set of notebooks to be executed. +```{tip} +Code cells can be tagged with `raises-exception` to let the executor known that a cell *may* raise an exception +(see [this issue on its behaviour](https://github.com/jupyter/nbconvert/issues/730)). +``` -Notebooks are recorded as pointers to their URI (e.g. file path), -i.e. no physical copying takes place until execution time. +## Retrieving executed notebooks -You can list the notebooks to see which have existing records in the cache (by hash), -and which will require execution: +Notebooks added to the project are not modified in any way during or after execution: -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project -:command: add -:args: tests/notebooks/basic.ipynb tests/notebooks/basic_failing.ipynb tests/notebooks/basic_unrun.ipynb tests/notebooks/complex_outputs.ipynb tests/notebooks/external_output.ipynb -``` +You can merge the cached outputs into a source notebook with: ```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project -:command: list +:command: merge +:args: tests/notebooks/basic.md _executed_notebook.ipynb ``` -You can remove a notebook from the project by its URI or ID: +## Specifying notebooks with assets + +When executing in a temporary directory, you may want to specify additional "asset" files that also need to be be copied to this directory for the notebook to run. ```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: remove -:args: 4 +:args: tests/notebooks/basic.ipynb ``` -You can then run a basic execution of the required notebooks: - -```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: remove -:args: 6 2 +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: add-with-assets +:args: -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt ``` -```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache -:command: execute +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: show +:args: tests/notebooks/basic.ipynb ``` -Successfully executed notebooks will be cached to the cache, -along with any 'artefacts' created by the execution, -that are inside the notebook folder, and data supplied by the executor. +## Adding notebooks directly to the cache -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project -:command: list -``` +Pre-executed notebooks can be added to the cache directly, without executing them. -Execution data (such as execution time) will be stored in the cache record: +A check will be made that the notebooks look to have been executed correctly, +i.e. the cell execution counts go sequentially up from 1. ```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: show -:args: 6 +:command: add +:args: tests/notebooks/complex_outputs.ipynb +:input: y ``` -Failed notebooks will not be cached, but the exception traceback will be added to the notebook's project record: +Or to skip the validation: -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project -:command: show -:args: 2 +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: add +:args: --no-validate tests/notebooks/external_output.ipynb ``` -```{tip} -Code cells can be tagged with `raises-exception` to let the executor known that a cell *may* raise an exception -(see [this issue on its behaviour](https://github.com/jupyter/nbconvert/issues/730)). +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: list ``` -Once executed you may leave notebooks in the project, for later re-execution, or remove them: +:::{tip} +To only show the latest versions of cached notebooks. -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project -:command: clear -:input: y +```console +$ jcache cache list --latest-only ``` -You can also add notebooks to the projects with assets; -external files that are required by the notebook during execution. -As with artefacts, these files must be in the same folder as the notebook, -or a sub-folder. +::: -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project -:command: add-with-assets -:args: -nb tests/notebooks/basic.ipynb tests/notebooks/artifact_folder/artifact.txt -``` +## Diffing notebooks -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project -:command: show -:args: 1 +You can diff any of the cached notebooks with any (external) notebook: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache +:command: diff-nb +:args: 1 tests/notebooks/basic_unrun.ipynb ``` diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index c8d3725..86b94f3 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -245,7 +245,13 @@ def __repr__(self): def to_dict(self): return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} - def format_dict(self, cache_record=None, path_length=None, assets=True): + def format_dict( + self, + cache_record: Optional[NbCacheRecord] = None, + path_length: Optional[int] = None, + assets: bool = True, + ) -> dict: + """Return data for display.""" status = "-" if cache_record: status = f"✅ [{cache_record.pk}]" diff --git a/jupyter_cache/cli/commands/cmd_project.py b/jupyter_cache/cli/commands/cmd_project.py index 7e98fe8..d542939 100644 --- a/jupyter_cache/cli/commands/cmd_project.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -1,3 +1,4 @@ +import os import sys import click @@ -64,7 +65,9 @@ def remove_nbs(cache_path, pk_paths): for pk_path in pk_paths: # TODO deal with errors (print all at end? or option to ignore) click.echo("Removing: {}".format(pk_path)) - db.remove_nb_from_project(int(pk_path) if pk_path.isdigit() else pk_path) + db.remove_nb_from_project( + int(pk_path) if pk_path.isdigit() else os.path.abspath(pk_path) + ) click.secho("Success!", fg="green") @@ -97,22 +100,24 @@ def list_nbs_in_project(cache_path, path_length, assets): @cmnd_project.command("show") @options.CACHE_PATH -@arguments.PK +@arguments.PK_OR_PATH @click.option( "--tb/--no-tb", default=True, show_default=True, help="Show traceback, if last execution failed.", ) -def show_project_record(cache_path, pk, tb): +def show_project_record(cache_path, pk_path, tb): """Show details of a notebook (by ID).""" import yaml db = get_cache(cache_path) try: - record = db.get_project_record(pk) + record = db.get_project_record( + int(pk_path) if pk_path.isdigit() else os.path.abspath(pk_path) + ) except KeyError: - click.secho("ID {} does not exist, Aborting!".format(pk), fg="red") + click.secho("ID {} does not exist, Aborting!".format(pk_path), fg="red") sys.exit(1) cache_record = db.get_cached_project_nb(record.uri) data = record.format_dict(cache_record=cache_record, path_length=None, assets=False) @@ -134,7 +139,9 @@ def show_project_record(cache_path, pk, tb): def merge_executed(cache_path, pk_path, outpath): """Write notebook merged with cached outputs (by ID/URI).""" db = get_cache(cache_path) - nb = db.get_project_notebook(int(pk_path) if pk_path.isdigit() else pk_path).nb + nb = db.get_project_notebook( + int(pk_path) if pk_path.isdigit() else os.path.abspath(pk_path) + ).nb cached_pk, nb = db.merge_match_into_notebook(nb) nbformat.write(nb, outpath) click.echo(f"Merged with cache PK {cached_pk}") diff --git a/setup.cfg b/setup.cfg index ed55cfc..c75a9e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,8 +47,8 @@ console_scripts = jcache.executors = local-serial = jupyter_cache.executors.basic:JupyterExecutorLocalSerial temp-serial = jupyter_cache.executors.basic:JupyterExecutorTempSerial - local-mproc = jupyter_cache.executors.basic:JupyterExecutorLocalMproc - temp-mproc = jupyter_cache.executors.basic:JupyterExecutorTempMproc + local-parallel = jupyter_cache.executors.basic:JupyterExecutorLocalMproc + temp-parallel = jupyter_cache.executors.basic:JupyterExecutorTempMproc jcache.readers = nbformat = jupyter_cache.readers:nbf_reader jupytext = jupyter_cache.readers:jupytext_reader @@ -60,6 +60,7 @@ cli = code_style = pre-commit~=2.12 rtd = + jupytext myst-nb~=0.12.3 sphinx-book-theme~=0.1.1 sphinx-copybutton diff --git a/tests/test_cache.py b/tests/test_cache.py index 41c6924..64562f8 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -183,7 +183,7 @@ def test_artifacts(tmp_path): @pytest.mark.parametrize( - "executor_key", ["local-serial", "temp-serial", "local-mproc", "temp-mproc"] + "executor_key", ["local-serial", "temp-serial", "local-parallel", "temp-parallel"] ) def test_execution(tmp_path, executor_key): from jupyter_cache.executors import load_executor From bd7493b10a4d4307f8d0d2cf67c4cc5ecaa94f3f Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 4 Aug 2021 17:41:28 +0200 Subject: [PATCH 16/39] =?UTF-8?q?=F0=9F=93=9A=20DOCS:=20Improve=20favicon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/_static/logo_square.svg | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_static/logo_square.svg b/docs/_static/logo_square.svg index bf1364e..00448c2 100644 --- a/docs/_static/logo_square.svg +++ b/docs/_static/logo_square.svg @@ -1,4 +1,5 @@ + From 263cfe473839f447c19b668628773f8867c7a7a6 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 13:38:38 +0100 Subject: [PATCH 17/39] fix org --- README.md | 2 +- docs/develop/contributing.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26dc5f5..c0b5061 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ pip install jupyter-cache[cli] For development: ```bash -git clone https://github.com/ExecutableBookProject/jupyter-cache +git clone https://github.com/executablebooks/jupyter-cache cd jupyter-cache git checkout develop pip install -e .[cli,code_style,testing] diff --git a/docs/develop/contributing.md b/docs/develop/contributing.md index d6a6717..e6ed0e2 100644 --- a/docs/develop/contributing.md +++ b/docs/develop/contributing.md @@ -11,7 +11,7 @@ For package development: ```bash -git clone https://github.com/ExecutableBookProject/jupyter-cache +git clone https://github.com/executablebooks/jupyter-cache cd jupyter-cache git checkout develop pip install -e .[cli,code_style,testing,rtd] From 5dc3fea898d9967d47cd97a72f4d37eca4c54076 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 13:41:37 +0100 Subject: [PATCH 18/39] Apply suggestions from code review Co-authored-by: Chris Holdgraf --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 5d11509..d1972cf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Jupyter Cache -This package provides an [API](use/api) and [CLI](use/cli) for executing and cacheing multiple Jupyter Notebook-like files. +Execute and cache multiple Jupyter Notebook-like files via an [API](use/api) and [CLI](use/cli). Smart re-execution : Notebooks will only be re-executed when **code cells** have changed (or code related metadata), not Markdown/Raw cells. From 6cb88ffdf8bb27cb5ada6e77a3b00dbe7f5785bc Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 13:47:29 +0100 Subject: [PATCH 19/39] Move nbdime out of install requirements --- jupyter_cache/cache/main.py | 7 ++++++- setup.cfg | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 482ca98..6e447e3 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -384,7 +384,12 @@ def diff_nbnode_with_cache( Note: this will not diff markdown content, since it is not stored in the cache. """ - import nbdime + try: + import nbdime + except ImportError: + raise ImportError( + "nbdime is required to diff notebooks, install with `pip install nbdime`" + ) from nbdime.prettyprint import PrettyPrintConfig, pretty_print_diff cached_nb = self.get_cache_bundle(pk).nb diff --git a/setup.cfg b/setup.cfg index c75a9e3..d78ac4e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,6 @@ install_requires = click importlib-metadata nbclient>=0.2,<0.6 - nbdime nbformat pyyaml sqlalchemy>=1.3.12,<1.5 @@ -65,6 +64,7 @@ rtd = sphinx-book-theme~=0.1.1 sphinx-copybutton testing = + nbdime coverage ipykernel jupytext From 8e291bf5a34031e79c2e1bd6db2a5d7fb1fe99d5 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 13:48:56 +0100 Subject: [PATCH 20/39] Remove click-completion click v8 has improved completion handling --- README.md | 2 +- docs/index.md | 2 +- jupyter_cache/cli/commands/__init__.py | 8 +------- setup.cfg | 1 - 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c0b5061..0115b98 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Some desired requirements (not yet all implemented): ## Install ```bash -pip install jupyter-cache[cli] +pip install jupyter-cache ``` For development: diff --git a/docs/index.md b/docs/index.md index d1972cf..26266ca 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ Execution reports Install `jupyter-cache`, via pip or Conda: ```bash -pip install jupyter-cache[cli] +pip install jupyter-cache ``` ```bash diff --git a/jupyter_cache/cli/commands/__init__.py b/jupyter_cache/cli/commands/__init__.py index b8ff253..b89ae69 100644 --- a/jupyter_cache/cli/commands/__init__.py +++ b/jupyter_cache/cli/commands/__init__.py @@ -1,10 +1,4 @@ -try: - import click_completion -except ImportError: - pass -else: - # Activate the completion of parameter types provided by the click_completion package - click_completion.init() +"""The jupyter-cache CLI.""" from .cmd_cache import * # noqa: F401,F403,E402 from .cmd_config import * # noqa: F401,F403,E402 diff --git a/setup.cfg b/setup.cfg index d78ac4e..4a13cc6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,7 +54,6 @@ jcache.readers = [options.extras_require] cli = - click-completion click-log code_style = pre-commit~=2.12 From 819f40c54d39380ee2d95f4ebe670df38cb6ed9f Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 13:52:54 +0100 Subject: [PATCH 21/39] Update index.md --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 26266ca..5fa769e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ conda install jupyter-cache ```{jcache-clear} ``` -Add one or more source notebook files to the "project": +Add one or more source notebook files to the "project" (a folder containing a database and a cache of executed notebooks): ```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: add From 8e8df2eb4d0f81639eaceae0d48863af2f3c0abe Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 13:54:28 +0100 Subject: [PATCH 22/39] Update index.md --- docs/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 5fa769e..69daa84 100644 --- a/docs/index.md +++ b/docs/index.md @@ -49,7 +49,6 @@ Now run the execution: ```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache :command: execute -:args: --executor local-serial ``` Successfully executed files will now be associated with a record in the cache: From b10fbfebed67e581bd1180d205ecc2984bfbf23a Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 14:00:09 +0100 Subject: [PATCH 23/39] update deps --- .github/workflows/tests.yml | 4 ++-- .pre-commit-config.yaml | 8 ++++---- setup.cfg | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 03ca95a..35e4f01 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,10 +26,10 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9, 3.10] include: - os: windows-latest - python-version: 3.7 + python-version: 3.8 runs-on: ${{ matrix.os }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c9e0415..1790269 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,8 +8,8 @@ exclude: > repos: - - repo: git://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 hooks: - id: check-json - id: check-yaml @@ -24,12 +24,12 @@ repos: additional_dependencies: [setuptools>=46.4.0] - repo: https://github.com/pycqa/isort - rev: 5.9.3 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/psf/black - rev: 21.9b0 + rev: 21.12b0 hooks: - id: black diff --git a/setup.cfg b/setup.cfg index 4a13cc6..28f94fe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,6 +58,7 @@ cli = code_style = pre-commit~=2.12 rtd = + nbdime jupytext myst-nb~=0.12.3 sphinx-book-theme~=0.1.1 From 5520614a4802bb053c78f2743839bd5408afd1c6 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 14:04:17 +0100 Subject: [PATCH 24/39] expose kwargs of execution --- jupyter_cache/executors/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jupyter_cache/executors/utils.py b/jupyter_cache/executors/utils.py index 8a8a91b..3eb14f9 100644 --- a/jupyter_cache/executors/utils.py +++ b/jupyter_cache/executors/utils.py @@ -1,7 +1,7 @@ import shutil import traceback from pathlib import Path -from typing import List, Optional, Union +from typing import Any, List, Optional, Union import attr from nbclient import execute as executenb @@ -28,6 +28,8 @@ def single_nb_execution( timeout: Optional[int], allow_errors: bool, meta_override: bool = True, + record_timing: bool = False, + **kwargs: Any, ) -> ExecutionResult: """Execute notebook in place. @@ -38,6 +40,7 @@ def single_nb_execution( execution is stopped and a ``CellExecutionError`` is raised. :param meta_override: If ``True`` then timeout and allow_errors may be overridden by equivalent keys in nb.metadata.execution + :param kwargs: Additional keyword arguments to pass to the ``NotebookClient``. :returns: The execution time in seconds """ @@ -57,7 +60,8 @@ def single_nb_execution( cwd=cwd, timeout=timeout, allow_errors=allow_errors, - record_timing=False, + record_timing=record_timing, + **kwargs, ) except (CellExecutionError, CellTimeoutError) as err: error = err From b58c14f79714b5de30c94387db088225f511bbe3 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 14:09:22 +0100 Subject: [PATCH 25/39] Update tests.yml --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 35e4f01..51e1d49 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python 3.8 uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: "3.8" - uses: pre-commit/action@v2.0.0 tests: @@ -26,10 +26,10 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9, 3.10] + python-version: ["3.7", "3.8", "3.9", "3.10"] include: - os: windows-latest - python-version: 3.8 + python-version: "3.8" runs-on: ${{ matrix.os }} @@ -70,7 +70,7 @@ jobs: - name: Set up Python 3.8 uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: "3.8" - name: Build package run: | pip install wheel From a2772c74a5ef2f9988cb11582d516b0eb56bafdc Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 14:10:37 +0100 Subject: [PATCH 26/39] Update tests.yml --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 51e1d49..95dfece 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -73,8 +73,8 @@ jobs: python-version: "3.8" - name: Build package run: | - pip install wheel - python setup.py sdist bdist_wheel + pip install build + python -m build - name: Publish uses: pypa/gh-action-pypi-publish@v1.1.0 with: From 7d7e25dc9c38ea198dac2d508664ce3c859e40ac Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 14:15:25 +0100 Subject: [PATCH 27/39] update pytest --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 28f94fe..a1e56f3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,10 +15,10 @@ classifiers = Intended Audience :: Developers License :: OSI Approved :: MIT License Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Topic :: Software Development :: Libraries :: Python Modules keywords = sphinx extension material design web components @@ -72,7 +72,7 @@ testing = nbformat>=5.1 numpy pandas - pytest>=3.6,<4 + pytest>=6,<7 pytest-cov pytest-regressions sympy From ca14dcf375fa75227544821b3618e605e80f68e1 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 14:24:30 +0100 Subject: [PATCH 28/39] diff-nb to diff --- docs/using/cli.md | 8 ++++++-- jupyter_cache/cli/commands/cmd_cache.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/using/cli.md b/docs/using/cli.md index 2afe2c8..c73038a 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -5,7 +5,7 @@ ```{jcache-clear} ``` -Note, you can follow this tutorial from the checked-out repository folder: +Note, you can follow this tutorial by cloning : ```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache :args: --help @@ -241,7 +241,11 @@ $ jcache cache list --latest-only You can diff any of the cached notebooks with any (external) notebook: +```{warning} +This requires `pip install nbdime` +``` + ```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: diff-nb +:command: diff :args: 1 tests/notebooks/basic_unrun.ipynb ``` diff --git a/jupyter_cache/cli/commands/cmd_cache.py b/jupyter_cache/cli/commands/cmd_cache.py index 290e501..e50b9fd 100644 --- a/jupyter_cache/cli/commands/cmd_cache.py +++ b/jupyter_cache/cli/commands/cmd_cache.py @@ -194,7 +194,7 @@ def remove_caches(cache_path, pks, remove_all): click.secho("Success!", fg="green") -@cmnd_cache.command("diff-nb") +@cmnd_cache.command("diff") @arguments.PK @arguments.NB_PATH @options.CACHE_PATH From 2c16de5bc5f06c5139e04c7b2943493641346e71 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 17:33:38 +0100 Subject: [PATCH 29/39] Make the stored read_data a dict So that we can make it more flexible, and allow for the myst-nb custom formats (with kwargs) --- docs/using/api.ipynb | 777 +++++++++++----------- jupyter_cache/base.py | 16 +- jupyter_cache/cache/db.py | 24 +- jupyter_cache/cache/main.py | 25 +- jupyter_cache/cli/commands/cmd_exec.py | 4 +- jupyter_cache/cli/commands/cmd_project.py | 19 +- jupyter_cache/readers.py | 49 +- jupyter_cache/utils.py | 30 +- tests/test_cache.py | 5 +- 9 files changed, 496 insertions(+), 453 deletions(-) diff --git a/docs/using/api.ipynb b/docs/using/api.ipynb index 858bd54..0bdf14e 100644 --- a/docs/using/api.ipynb +++ b/docs/using/api.ipynb @@ -2,15 +2,16 @@ "cells": [ { "cell_type": "markdown", + "metadata": {}, "source": [ "(use/api)=\n", "\n", "# Python API" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "This page outlines how to utilise the cache programatically.\n", "We step throught the three aspects illustrated in the diagram below:\n", @@ -21,29 +22,30 @@ "\n", "Illustration of the execution process.\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "```{note}\n", "The full Jupyter notebook for this page can accessed here; {nb-download}`api.ipynb`.\n", "Try it for yourself!\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "## Initialisation" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 1, + "metadata": {}, + "outputs": [], "source": [ "from pathlib import Path\n", "import nbformat as nbf\n", @@ -54,103 +56,102 @@ " tabulate_cache_records, \n", " tabulate_project_records\n", ")" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "First we setup a cache and ensure that it is cleared.\n", "\n", "```{important}\n", "Clearing a cache wipes its entire content, including any settings (such as cache limit).\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 2, - "source": [ - "cache = get_cache(\".jupyter_cache\")\n", - "cache.clear_cache()\n", - "cache" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "JupyterCacheBase('/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/.jupyter_cache')" ] }, + "execution_count": 2, "metadata": {}, - "execution_count": 2 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache = get_cache(\".jupyter_cache\")\n", + "cache.clear_cache()\n", + "cache" + ] }, { "cell_type": "code", "execution_count": 3, - "source": [ - "print(cache.list_cache_records())\n", - "print(cache.list_project_records())" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "[]\n", "[]\n" ] } ], - "metadata": {} + "source": [ + "print(cache.list_cache_records())\n", + "print(cache.list_project_records())" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "(use/api/cache)=\n", "\n", "## Cacheing Notebooks" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "To directly cache a notebook:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 4, - "source": [ - "record = cache.cache_notebook_file(\n", - " path=Path(\"example_nbs\", \"basic.ipynb\")\n", - ")\n", - "record" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, + "execution_count": 4, "metadata": {}, - "execution_count": 4 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "record = cache.cache_notebook_file(\n", + " path=Path(\"example_nbs\", \"basic.ipynb\")\n", + ")\n", + "record" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "This will add a physical copy of the notebook to tha cache (stripped of any text cells) and return the record that has been added to the cache database.\n", "\n", @@ -159,128 +160,124 @@ "```\n", "\n", "The record stores metadata for the notebook:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 5, - "source": [ - "record.to_dict()" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ - "{'pk': 1,\n", - " 'uri': 'example_nbs/basic.ipynb',\n", - " 'data': {},\n", - " 'accessed': datetime.datetime(2021, 8, 4, 2, 23, 2, 177344),\n", - " 'description': '',\n", + "{'description': '',\n", " 'hashkey': '94c17138f782c75df59e989fffa64e3a',\n", - " 'created': datetime.datetime(2021, 8, 4, 2, 23, 2, 177333)}" + " 'created': datetime.datetime(2022, 1, 12, 15, 15, 27, 255299),\n", + " 'accessed': datetime.datetime(2022, 1, 12, 15, 15, 27, 255312),\n", + " 'data': {},\n", + " 'uri': 'example_nbs/basic.ipynb',\n", + " 'pk': 1}" ] }, + "execution_count": 5, "metadata": {}, - "execution_count": 5 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "record.to_dict()" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "```{important}\n", "The URI that the notebook is read from is stored, but does not have an impact on later comparison of notebooks. They are only compared by their internal content.\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "We can retrive cache records by their Primary Key (pk): " - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 6, - "source": [ - "cache.list_cache_records()" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "[NbCacheRecord(pk=1)]" ] }, + "execution_count": 6, "metadata": {}, - "execution_count": 6 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.list_cache_records()" + ] }, { "cell_type": "code", "execution_count": 7, - "source": [ - "cache.get_cache_record(1)" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, + "execution_count": 7, "metadata": {}, - "execution_count": 7 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.get_cache_record(1)" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "To load the entire notebook that is related to a pk:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 8, - "source": [ - "nb_bundle = cache.get_cache_bundle(1)\n", - "nb_bundle" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "CacheBundleOut(nb=Notebook(cells=1), record=NbCacheRecord(pk=1), artifacts=NbArtifacts(paths=0))" ] }, + "execution_count": 8, "metadata": {}, - "execution_count": 8 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "nb_bundle = cache.get_cache_bundle(1)\n", + "nb_bundle" + ] }, { "cell_type": "code", "execution_count": 9, - "source": [ - "nb_bundle.nb" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "{'cells': [{'cell_type': 'code',\n", @@ -303,66 +300,65 @@ " 'nbformat_minor': 2}" ] }, + "execution_count": 9, "metadata": {}, - "execution_count": 9 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "nb_bundle.nb" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Trying to add a notebook to the cache that matches an existing one will result in a error, since the cache ensures that all notebook hashes are unique:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 10, - "source": [ - "record = cache.cache_notebook_file(\n", - " path=Path(\"example_nbs\", \"basic.ipynb\")\n", - ")" - ], + "metadata": { + "tags": [ + "raises-exception" + ] + }, "outputs": [ { - "output_type": "error", "ename": "CachingError", "evalue": "Notebook already exists in cache and overwrite=False.", + "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mCachingError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_34728/3576020660.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m record = cache.cache_notebook_file(\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"example_nbs\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"basic.ipynb\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m )\n", + "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_99993/3576020660.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m record = cache.cache_notebook_file(\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mPath\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"example_nbs\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"basic.ipynb\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m )\n", "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_file\u001b[0;34m(self, path, uri, artifacts, data, check_validity, overwrite)\u001b[0m\n\u001b[1;32m 268\u001b[0m \"\"\"\n\u001b[1;32m 269\u001b[0m \u001b[0mnotebook\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnbf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnbf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mNO_CONVERT\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 270\u001b[0;31m return self.cache_notebook_bundle(\n\u001b[0m\u001b[1;32m 271\u001b[0m CacheBundleIn(\n\u001b[1;32m 272\u001b[0m \u001b[0mnotebook\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mcache_notebook_bundle\u001b[0;34m(self, bundle, check_validity, overwrite, description)\u001b[0m\n\u001b[1;32m 213\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexists\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 214\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0moverwrite\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 215\u001b[0;31m raise CachingError(\n\u001b[0m\u001b[1;32m 216\u001b[0m \u001b[0;34m\"Notebook already exists in cache and overwrite=False.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 217\u001b[0m )\n", "\u001b[0;31mCachingError\u001b[0m: Notebook already exists in cache and overwrite=False." ] } ], - "metadata": { - "tags": [ - "raises-exception" - ] - } + "source": [ + "record = cache.cache_notebook_file(\n", + " path=Path(\"example_nbs\", \"basic.ipynb\")\n", + ")" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "If we load a notebook external to the cache, then we can try to match it to one\n", "stored inside the cache:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 11, - "source": [ - "notebook = nbf.read(str(Path(\"example_nbs\", \"basic.ipynb\")), 4)\n", - "notebook" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "{'cells': [{'cell_type': 'markdown',\n", @@ -388,128 +384,130 @@ " 'nbformat_minor': 2}" ] }, + "execution_count": 11, "metadata": {}, - "execution_count": 11 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "notebook = nbf.read(str(Path(\"example_nbs\", \"basic.ipynb\")), 4)\n", + "notebook" + ] }, { "cell_type": "code", "execution_count": 12, - "source": [ - "cache.match_cache_notebook(notebook)" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, + "execution_count": 12, "metadata": {}, - "execution_count": 12 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.match_cache_notebook(notebook)" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Notebooks are matched by a hash based only on aspects of the notebook that will affect its execution (and hence outputs). So changing text cells will match the cached notebook:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 13, + "metadata": {}, + "outputs": [], "source": [ "notebook.cells[0].source = \"change some text\"" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 14, - "source": [ - "cache.match_cache_notebook(notebook)" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, + "execution_count": 14, "metadata": {}, - "execution_count": 14 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.match_cache_notebook(notebook)" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "But changing code cells will result in a different hash, and so will not be matched:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 15, + "metadata": {}, + "outputs": [], "source": [ "notebook.cells[1].source = \"change some source code\"" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 16, - "source": [ - "cache.match_cache_notebook(notebook)" - ], + "metadata": { + "tags": [ + "raises-exception" + ] + }, "outputs": [ { - "output_type": "error", "ename": "KeyError", "evalue": "'Cache record not found for NB with hashkey: 07e6a47c8c180cb7851ede6dbb088769'", + "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_34728/941642554.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mcache\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmatch_cache_notebook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnotebook\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_99993/941642554.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mcache\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmatch_cache_notebook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnotebook\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/main.py\u001b[0m in \u001b[0;36mmatch_cache_notebook\u001b[0;34m(self, nb)\u001b[0m\n\u001b[1;32m 333\u001b[0m \"\"\"\n\u001b[1;32m 334\u001b[0m \u001b[0m_\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhashkey\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcreate_hashed_notebook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 335\u001b[0;31m \u001b[0mcache_record\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mNbCacheRecord\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrecord_from_hashkey\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhashkey\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 336\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mcache_record\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 337\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/Documents/GitHub/jupyter-cache/jupyter_cache/cache/db.py\u001b[0m in \u001b[0;36mrecord_from_hashkey\u001b[0;34m(hashkey, db)\u001b[0m\n\u001b[1;32m 158\u001b[0m )\n\u001b[1;32m 159\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 160\u001b[0;31m raise KeyError(\n\u001b[0m\u001b[1;32m 161\u001b[0m \u001b[0;34m\"Cache record not found for NB with hashkey: {}\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhashkey\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 162\u001b[0m )\n", "\u001b[0;31mKeyError\u001b[0m: 'Cache record not found for NB with hashkey: 07e6a47c8c180cb7851ede6dbb088769'" ] } ], - "metadata": { - "tags": [ - "raises-exception" - ] - } + "source": [ + "cache.match_cache_notebook(notebook)" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "To understand the difference between an external notebook, and one stored in the cache, we can 'diff' them:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 17, - "source": [ - "print(cache.diff_nbnode_with_cache(1, notebook, as_str=True))" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "nbdiff\n", "--- cached pk=1\n", @@ -543,110 +541,113 @@ ] } ], - "metadata": {} + "source": [ + "print(cache.diff_nbnode_with_cache(1, notebook, as_str=True))" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "If we cache this altered notebook, note that this will not remove the previously cached notebook:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 18, - "source": [ - "nb_bundle = CacheBundleIn(\n", - " nb=notebook,\n", - " uri=Path(\"example_nbs\", \"basic.ipynb\"),\n", - " data={\"tag\": \"mytag\"}\n", - ")\n", - "cache.cache_notebook_bundle(nb_bundle)" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=2)" ] }, + "execution_count": 18, "metadata": {}, - "execution_count": 18 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "nb_bundle = CacheBundleIn(\n", + " nb=notebook,\n", + " uri=Path(\"example_nbs\", \"basic.ipynb\"),\n", + " data={\"tag\": \"mytag\"}\n", + ")\n", + "cache.cache_notebook_bundle(nb_bundle)" + ] }, { "cell_type": "code", "execution_count": 19, - "source": [ - "print(tabulate_cache_records(\n", - " cache.list_cache_records(), path_length=1, hashkeys=True\n", - "))" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ " ID Origin URI Created Accessed Hashkey\n", "---- ------------ ---------------- ---------------- --------------------------------\n", - " 2 basic.ipynb 2021-08-04 02:25 2021-08-04 02:25 07e6a47c8c180cb7851ede6dbb088769\n", - " 1 basic.ipynb 2021-08-04 02:23 2021-08-04 02:25 94c17138f782c75df59e989fffa64e3a\n" + " 2 basic.ipynb 2022-01-12 15:16 2022-01-12 15:16 07e6a47c8c180cb7851ede6dbb088769\n", + " 1 basic.ipynb 2022-01-12 15:15 2022-01-12 15:16 94c17138f782c75df59e989fffa64e3a\n" ] } ], - "metadata": {} + "source": [ + "print(tabulate_cache_records(\n", + " cache.list_cache_records(), path_length=1, hashkeys=True\n", + "))" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Notebooks are retained in the cache, until the cache limit is reached,\n", "at which point the oldest notebooks are removed." - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 20, - "source": [ - "cache.get_cache_limit()" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "1000" ] }, + "execution_count": 20, "metadata": {}, - "execution_count": 20 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.get_cache_limit()" + ] }, { "cell_type": "code", "execution_count": 21, + "metadata": {}, + "outputs": [], "source": [ "cache.change_cache_limit(100)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "(use/api/project)=\n", "\n", "## Staging Notebooks for Execution" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Notebooks can be staged, by adding the path as a stage record.\n", "\n", @@ -654,105 +655,105 @@ "This does not physically add the notebook to the cache,\n", "merely store its URI, for later use.\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 22, - "source": [ - "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", - "record" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "NbProjectRecord(pk=1)" ] }, + "execution_count": 22, "metadata": {}, - "execution_count": 22 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", + "record" + ] }, { "cell_type": "code", "execution_count": 23, - "source": [ - "record.to_dict()" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "{'uri': '/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb',\n", " 'assets': [],\n", - " 'created': datetime.datetime(2021, 8, 4, 2, 25, 42, 410359),\n", - " 'reader': 'nbformat',\n", - " 'pk': 1,\n", - " 'traceback': ''}" + " 'created': datetime.datetime(2022, 1, 12, 15, 16, 27, 64960),\n", + " 'traceback': '',\n", + " 'read_data': {'name': 'nbformat', 'type': 'plugin'},\n", + " 'pk': 1}" ] }, + "execution_count": 23, "metadata": {}, - "execution_count": 23 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "record.to_dict()" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "If the staged notbook relates to one in the cache, we will be able to retrieve the cache record:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 24, - "source": [ - "cache.get_cached_project_nb(1)" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "NbCacheRecord(pk=1)" ] }, + "execution_count": 24, "metadata": {}, - "execution_count": 24 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.get_cached_project_nb(1)" + ] }, { "cell_type": "code", "execution_count": 25, - "source": [ - "print(tabulate_project_records(\n", - " cache.list_project_records(), path_length=2, cache=cache\n", - "))" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ " ID URI Reader Added Status\n", "---- ----------------------- -------- ---------------- --------\n", - " 1 example_nbs/basic.ipynb nbformat 2021-08-04 02:25 ✅ [1]\n" + " 1 example_nbs/basic.ipynb nbformat 2022-01-12 15:16 ✅ [1]\n" ] } ], - "metadata": {} + "source": [ + "print(tabulate_project_records(\n", + " cache.list_project_records(), path_length=2, cache=cache\n", + "))" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "We can also retrieve a *merged* notebook.\n", "This is a copy of the source notebook with the following added to it from the cached notebook:\n", @@ -762,22 +763,14 @@ " (only selected metadata can be merged if `cell_meta` is not `None`)\n", " \n", "In this way we create a notebook that is *fully* up-to-date for both its code and textual content:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 26, - "source": [ - "cache.merge_match_into_file(\n", - " cache.get_project_record(1).uri,\n", - " nb_meta=('kernelspec', 'language_info', 'widgets'),\n", - " cell_meta=None\n", - ")" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "(1,\n", @@ -804,190 +797,198 @@ " 'nbformat_minor': 2})" ] }, + "execution_count": 26, "metadata": {}, - "execution_count": 26 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.merge_match_into_file(\n", + " cache.get_project_record(1).uri,\n", + " nb_meta=('kernelspec', 'language_info', 'widgets'),\n", + " cell_meta=None\n", + ")" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "If we add a notebook that cannot be found in the cache, it will be listed for execution:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 27, - "source": [ - "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic_failing.ipynb\"))\n", - "record" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "NbProjectRecord(pk=2)" ] }, + "execution_count": 27, "metadata": {}, - "execution_count": 27 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "record = cache.add_nb_to_project(Path(\"example_nbs\", \"basic_failing.ipynb\"))\n", + "record" + ] }, { "cell_type": "code", "execution_count": 28, + "metadata": {}, + "outputs": [], "source": [ "cache.get_cached_project_nb(2) # returns None" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 29, - "source": [ - "cache.list_unexecuted()" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "[NbProjectRecord(pk=2)]" ] }, + "execution_count": 29, "metadata": {}, - "execution_count": 29 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.list_unexecuted()" + ] }, { "cell_type": "code", "execution_count": 30, - "source": [ - "print(tabulate_project_records(\n", - " cache.list_project_records(), path_length=2, cache=cache\n", - "))" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ " ID URI Reader Added Status\n", "---- ------------------------------- -------- ---------------- --------\n", - " 1 example_nbs/basic.ipynb nbformat 2021-08-04 02:25 ✅ [1]\n", - " 2 example_nbs/basic_failing.ipynb nbformat 2021-08-04 02:26 -\n" + " 1 example_nbs/basic.ipynb nbformat 2022-01-12 15:16 ✅ [1]\n", + " 2 example_nbs/basic_failing.ipynb nbformat 2022-01-12 15:17 -\n" ] } ], - "metadata": {} + "source": [ + "print(tabulate_project_records(\n", + " cache.list_project_records(), path_length=2, cache=cache\n", + "))" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "To remove a notebook from the staging area:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 31, + "metadata": {}, + "outputs": [], "source": [ "cache.remove_nb_from_project(1)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 32, - "source": [ - "print(tabulate_project_records(\n", - " cache.list_project_records(), path_length=2, cache=cache\n", - "))" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ " ID URI Reader Added Status\n", "---- ------------------------------- -------- ---------------- --------\n", - " 2 example_nbs/basic_failing.ipynb nbformat 2021-08-04 02:26 -\n" + " 2 example_nbs/basic_failing.ipynb nbformat 2022-01-12 15:17 -\n" ] } ], - "metadata": {} + "source": [ + "print(tabulate_project_records(\n", + " cache.list_project_records(), path_length=2, cache=cache\n", + "))" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "(use/api/execute)=\n", "\n", "## Execution" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "If we have some staged notebooks:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 33, - "source": [ - "cache.clear_cache()\n", - "cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", - "cache.add_nb_to_project(Path(\"example_nbs\", \"basic_failing.ipynb\"))" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "NbProjectRecord(pk=2)" ] }, + "execution_count": 33, "metadata": {}, - "execution_count": 33 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.clear_cache()\n", + "cache.add_nb_to_project(Path(\"example_nbs\", \"basic.ipynb\"))\n", + "cache.add_nb_to_project(Path(\"example_nbs\", \"basic_failing.ipynb\"))" + ] }, { "cell_type": "code", "execution_count": 34, - "source": [ - "print(tabulate_project_records(\n", - " cache.list_project_records(), path_length=2, cache=cache\n", - "))" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ " ID URI Reader Added Status\n", "---- ------------------------------- -------- ---------------- --------\n", - " 1 example_nbs/basic.ipynb nbformat 2021-08-04 02:26 -\n", - " 2 example_nbs/basic_failing.ipynb nbformat 2021-08-04 02:26 -\n" + " 1 example_nbs/basic.ipynb nbformat 2022-01-12 15:17 -\n", + " 2 example_nbs/basic_failing.ipynb nbformat 2022-01-12 15:17 -\n" ] } ], - "metadata": {} + "source": [ + "print(tabulate_project_records(\n", + " cache.list_project_records(), path_length=2, cache=cache\n", + "))" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Then we can select an executor (specified as entry points) to execute the notebook.\n", "\n", @@ -995,55 +996,55 @@ "To view the executors log, make sure logging is enabled,\n", "or you can parse a logger directly to `load_executor()`.\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 35, - "source": [ - "list_executors()" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "{'local-parallel', 'local-serial', 'temp-parallel', 'temp-serial'}" ] }, + "execution_count": 35, "metadata": {}, - "execution_count": 35 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "list_executors()" + ] }, { "cell_type": "code", "execution_count": 36, - "source": [ - "from logging import basicConfig, INFO\n", - "basicConfig(level=INFO)\n", - "\n", - "executor = load_executor(\"local-serial\", cache=cache)\n", - "executor" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "JupyterExecutorLocalSerial(cache=JupyterCacheBase('/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/.jupyter_cache'))" ] }, + "execution_count": 36, "metadata": {}, - "execution_count": 36 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "from logging import basicConfig, INFO\n", + "basicConfig(level=INFO)\n", + "\n", + "executor = load_executor(\"local-serial\", cache=cache)\n", + "executor" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Calling `run_and_cache()` will run all staged notebooks that do not already have matches in the cache.\n", "It will return a dictionary with lists for:\n", @@ -1062,20 +1063,16 @@ "You can use the `filter_uris` and/or `filter_pks` options to only run selected staged notebooks.\n", "You can also specify the timeout for execution in seconds using the `timeout` option.\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 37, - "source": [ - "result = executor.run_and_cache()\n", - "result" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stderr", + "output_type": "stream", "text": [ "INFO:jupyter_cache.executors.base:Executing 2 notebook(s) in serial\n", "INFO:jupyter_cache.executors.base:Executing: /Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb\n", @@ -1089,7 +1086,7 @@ "\n", "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n", "\u001b[0;31mException\u001b[0m Traceback (most recent call last)\n", - "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_80205/340246212.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", + "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_1308/340246212.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", "\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'oopsie!'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0m\n", "\u001b[0;31mException\u001b[0m: oopsie!\n", @@ -1098,113 +1095,113 @@ ] }, { - "output_type": "execute_result", "data": { "text/plain": [ "ExecutorRunResult(succeeded=['/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb'], excepted=['/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic_failing.ipynb'], errored=[])" ] }, + "execution_count": 37, "metadata": {}, - "execution_count": 37 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "result = executor.run_and_cache()\n", + "result" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Successfully executed notebooks will be added to the cache, and data about their execution (such as time taken) will be stored in the cache record:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 38, - "source": [ - "cache.list_cache_records()" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ "[NbCacheRecord(pk=1)]" ] }, + "execution_count": 38, "metadata": {}, - "execution_count": 38 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "cache.list_cache_records()" + ] }, { "cell_type": "code", "execution_count": 39, - "source": [ - "record = cache.get_cache_record(1)\n", - "record.to_dict()" - ], + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": [ - "{'pk': 1,\n", - " 'uri': '/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb',\n", - " 'data': {'execution_seconds': 1.5897068880003644},\n", - " 'accessed': datetime.datetime(2021, 8, 4, 2, 27, 39, 528685),\n", - " 'description': '',\n", + "{'description': '',\n", " 'hashkey': '94c17138f782c75df59e989fffa64e3a',\n", - " 'created': datetime.datetime(2021, 8, 4, 2, 27, 39, 528680)}" + " 'created': datetime.datetime(2022, 1, 12, 15, 17, 45, 471862),\n", + " 'accessed': datetime.datetime(2022, 1, 12, 15, 17, 45, 471871),\n", + " 'data': {'execution_seconds': 1.8344826350000005},\n", + " 'uri': '/Users/chrisjsewell/Documents/GitHub/jupyter-cache/docs/using/example_nbs/basic.ipynb',\n", + " 'pk': 1}" ] }, + "execution_count": 39, "metadata": {}, - "execution_count": 39 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "record = cache.get_cache_record(1)\n", + "record.to_dict()" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "Notebooks which failed to run will **not** be added to the cache,\n", "but details about their execution (including the exception traceback)\n", "will be added to the stage record:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 40, - "source": [ - "record = cache.get_project_record(2)\n", - "print(record.traceback)" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "Traceback (most recent call last):\n", - " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/jupyter_cache/executors/utils.py\", line 55, in single_nb_execution\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/jupyter_cache/executors/utils.py\", line 58, in single_nb_execution\n", " executenb(\n", - " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 1112, in execute\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 1093, in execute\n", " return NotebookClient(nb=nb, resources=resources, km=km, **kwargs).execute()\n", - " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/util.py\", line 74, in wrapped\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/util.py\", line 84, in wrapped\n", " return just_run(coro(*args, **kwargs))\n", - " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/util.py\", line 53, in just_run\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/util.py\", line 62, in just_run\n", " return loop.run_until_complete(coro)\n", - " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nest_asyncio.py\", line 70, in run_until_complete\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nest_asyncio.py\", line 81, in run_until_complete\n", " return f.result()\n", " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/asyncio/futures.py\", line 178, in result\n", " raise self._exception\n", " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/asyncio/tasks.py\", line 280, in __step\n", " result = coro.send(None)\n", - " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 553, in async_execute\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 559, in async_execute\n", " await self.async_execute_cell(\n", - " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 857, in async_execute_cell\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 854, in async_execute_cell\n", " self._check_raise_for_error(cell, exec_reply)\n", - " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 760, in _check_raise_for_error\n", + " File \"/Users/chrisjsewell/Documents/GitHub/jupyter-cache/.tox/py38/lib/python3.8/site-packages/nbclient/client.py\", line 756, in _check_raise_for_error\n", " raise CellExecutionError.from_cell_and_msg(cell, exec_reply_content)\n", "nbclient.exceptions.CellExecutionError: An error occurred while executing the following cell:\n", "------------------\n", @@ -1213,7 +1210,7 @@ "\n", "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n", "\u001b[0;31mException\u001b[0m Traceback (most recent call last)\n", - "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_80205/340246212.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", + "\u001b[0;32m/var/folders/t2/xbl15_3n4tsb1vr_ccmmtmbr0000gn/T/ipykernel_1308/340246212.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n", "\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'oopsie!'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0m\n", "\u001b[0;31mException\u001b[0m: oopsie!\n", @@ -1223,60 +1220,64 @@ ] } ], - "metadata": {} + "source": [ + "record = cache.get_project_record(2)\n", + "print(record.traceback)" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "We now have two staged records, and one cache record:" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 41, - "source": [ - "print(tabulate_project_records(\n", - " cache.list_project_records(), path_length=2, cache=cache\n", - "))" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ " ID URI Reader Added Status\n", "---- ------------------------------- -------- ---------------- --------\n", - " 1 example_nbs/basic.ipynb nbformat 2021-08-04 02:26 ✅ [1]\n", - " 2 example_nbs/basic_failing.ipynb nbformat 2021-08-04 02:26 ❌\n" + " 1 example_nbs/basic.ipynb nbformat 2022-01-12 15:17 ✅ [1]\n", + " 2 example_nbs/basic_failing.ipynb nbformat 2022-01-12 15:17 ❌\n" ] } ], - "metadata": {} + "source": [ + "print(tabulate_project_records(\n", + " cache.list_project_records(), path_length=2, cache=cache\n", + "))" + ] }, { "cell_type": "code", "execution_count": 42, - "source": [ - "print(tabulate_cache_records(\n", - " cache.list_cache_records(), path_length=1, hashkeys=True\n", - "))" - ], + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ " ID Origin URI Created Accessed Hashkey\n", "---- ------------ ---------------- ---------------- --------------------------------\n", - " 1 basic.ipynb 2021-08-04 02:27 2021-08-04 02:27 94c17138f782c75df59e989fffa64e3a\n" + " 1 basic.ipynb 2022-01-12 15:17 2022-01-12 15:17 94c17138f782c75df59e989fffa64e3a\n" ] } ], - "metadata": {} + "source": [ + "print(tabulate_cache_records(\n", + " cache.list_cache_records(), path_length=1, hashkeys=True\n", + "))" + ] }, { "cell_type": "markdown", + "metadata": {}, "source": [ "### Timeout\n", "A **timeout** argument can also be passed to `run_and_cache()` which takes value in seconds.\n", @@ -1290,30 +1291,30 @@ "```{note}\n", "Timeout specified in notebook metadata will take precedence over the one passed as an argument to `run_and_cache()`.\n", "```" - ], - "metadata": {} + ] } ], "metadata": { "celltoolbar": "Tags", - "kernelspec": { - "name": "python3", - "display_name": "Python 3.8.6 64-bit (conda)" - }, "interpreter": { - "hash": "fb9a14a894377db1cfdad9c4af43de04ce5a0728c4aa470d5611ebb394042755" + "hash": "8398b65b1e6feb38b0506d5ab1aedf8bf63748a9844a9c81ed9242850234e24f" + }, + "kernelspec": { + "display_name": "Coconut", + "language": "python", + "name": "python3" }, "language_info": { - "name": "python", - "version": "3.8.10", - "mimetype": "text/x-python", "codemirror_mode": { "name": "ipython", "version": 3 }, - "pygments_lexer": "ipython3", + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", "nbconvert_exporter": "python", - "file_extension": ".py" + "pygments_lexer": "ipython3", + "version": "3.8.12" } }, "nbformat": 4, diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index 92aadf9..563da1a 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -6,7 +6,7 @@ import io from abc import ABC, abstractmethod from pathlib import Path -from typing import Iterable, List, Optional, Tuple, Union +from typing import Iterable, List, Mapping, Optional, Tuple, Union import attr import nbformat as nbf @@ -14,6 +14,7 @@ # TODO make these abstract from jupyter_cache.cache.db import NbCacheRecord, NbProjectRecord +from jupyter_cache.readers import DEFAULT_READ_DATA NB_VERSION = 4 @@ -273,12 +274,16 @@ def diff_nbfile_with_cache( @abstractmethod def add_nb_to_project( - self, uri: str, *, reader: str = "nbformat", assets: List[str] = () + self, + uri: str, + *, + read_data: Mapping = DEFAULT_READ_DATA, + assets: List[str] = () ) -> NbProjectRecord: """Add a single notebook to the project. :param uri: The path to the file - :param reader: A key for the reader function, to read the uri and return a NotebookNode + :param read_data: Data to generate a function, to read the uri and return a NotebookNode :param assets: The path of files required by the notebook to run. :raises ValueError: assets not within the same folder as the notebook URI. """ @@ -297,7 +302,10 @@ def get_project_record(self, uri_or_pk: Union[int, str]) -> NbProjectRecord: @abstractmethod def get_project_notebook(self, uri_or_pk: Union[int, str]) -> ProjectNb: - """Return a single notebook in the project, by its primary key or URI.""" + """Return a single notebook in the project, by its primary key or URI. + + :raises NbReadError: if the notebook cannot be read + """ @abstractmethod def get_cached_project_nb( diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 86b94f3..93a1c3f 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from datetime import datetime from pathlib import Path -from typing import List, Optional +from typing import Any, Dict, List, Optional from sqlalchemy import JSON, Column, DateTime, Integer, String, Text from sqlalchemy.engine import Engine, create_engine @@ -19,7 +19,7 @@ DB_VERSION = 2 # v2: # - table: nbstage -> nbproject -# - added reader field to nbproject +# - added read_data field to nbproject def create_db(path, name="global.db") -> Engine: @@ -234,7 +234,7 @@ class NbProjectRecord(OrmBase): pk = Column(Integer(), primary_key=True) uri = Column(String(255), nullable=False, unique=True) - reader = Column(String(255), nullable=False) + read_data = Column(JSON(), nullable=False) assets = Column(JSON(), nullable=False, default=list) traceback = Column(Text(), nullable=True, default="") created = Column(DateTime, nullable=False, default=datetime.utcnow) @@ -250,6 +250,8 @@ def format_dict( cache_record: Optional[NbCacheRecord] = None, path_length: Optional[int] = None, assets: bool = True, + read_error: Optional[str] = None, + read_name: bool = True, ) -> dict: """Return data for display.""" status = "-" @@ -257,10 +259,12 @@ def format_dict( status = f"✅ [{cache_record.pk}]" elif self.traceback: status = "❌" + elif read_error: + status = "❗️ (unreadable)" data = { "ID": self.pk, "URI": str(shorten_path(self.uri, path_length)), - "Reader": self.reader, + "Reader": self.read_data.get("name", "-") if read_name else self.read_data, "Added": self.created.isoformat(" ", "minutes"), "Status": status, } @@ -268,6 +272,14 @@ def format_dict( data["Assets"] = len(self.assets) return data + @validates("read_data") + def validate_read_data(self, key, value): + if not isinstance(value, dict): + raise ValueError("read_data must be a dict") + if "name" not in value: + raise ValueError("read_data must have a name") + return value + @validates("assets") def validator_assets(self, key, value): return self.validate_assets(value) @@ -294,14 +306,14 @@ def validate_assets(paths, uri=None): def create_record( uri: str, db: Engine, + read_data: Dict[str, Any], raise_on_exists=True, *, - reader: str = "nbformat", assets=(), ) -> "NbProjectRecord": assets = NbProjectRecord.validate_assets(assets, uri) with session_context(db) as session: # type: Session - record = NbProjectRecord(uri=uri, reader=reader, assets=assets) + record = NbProjectRecord(uri=uri, read_data=read_data, assets=assets) session.add(record) try: session.commit() diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 6e447e3..8cf81bf 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -4,7 +4,7 @@ import shutil from contextlib import contextmanager from pathlib import Path -from typing import Iterable, List, Optional, Tuple, Union +from typing import Iterable, List, Mapping, Optional, Tuple, Union import nbformat as nbf @@ -19,7 +19,7 @@ ProjectNb, RetrievalError, ) -from jupyter_cache.readers import get_reader +from jupyter_cache.readers import DEFAULT_READ_DATA, NbReadError, get_reader from jupyter_cache.utils import to_relative_paths from .db import NbCacheRecord, NbProjectRecord, Setting, create_db @@ -406,16 +406,21 @@ def diff_nbnode_with_cache( return stream.getvalue() def add_nb_to_project( - self, path: str, *, reader: str = "nbformat", assets: List[str] = () + self, + path: str, + *, + read_data: Mapping = DEFAULT_READ_DATA, + assets: List[str] = (), ) -> NbProjectRecord: # check the reader can be loaded - _ = get_reader(reader) + read_data = dict(read_data) + _ = get_reader(read_data) # TODO should we test that the file can be read by the reader? return NbProjectRecord.create_record( str(Path(path).absolute()), self.db, raise_on_exists=False, - reader=reader, + read_data=read_data, assets=assets, ) # TODO physically copy to cache? @@ -448,8 +453,14 @@ def get_project_notebook(self, uri_or_pk: Union[int, str]) -> ProjectNb: raise IOError( "The URI of the project record no longer exists: {}".format(record.uri) ) - converter = get_reader(record.reader) - notebook = converter(record.uri) + try: + reader = get_reader(record.read_data) + notebook = reader(record.uri) + assert isinstance( + notebook, nbf.NotebookNode + ), f"Reader did not return a v4 NotebookNode: {type(notebook)} {notebook}" + except Exception as exc: + raise NbReadError(f"Failed to read the notebook: {exc}") from exc return ProjectNb(record.pk, record.uri, notebook, record.assets) def get_cached_project_nb( diff --git a/jupyter_cache/cli/commands/cmd_exec.py b/jupyter_cache/cli/commands/cmd_exec.py index 1ab9ebf..67be5fd 100644 --- a/jupyter_cache/cli/commands/cmd_exec.py +++ b/jupyter_cache/cli/commands/cmd_exec.py @@ -54,7 +54,9 @@ def execute_nbs(cache_path, executor, pk_paths, timeout): show_default=True, ) for pk_path in not_in_project: - record = db.add_nb_to_project(pk_path, reader=reader) + record = db.add_nb_to_project( + pk_path, read_data={"name": reader, "type": "plugin"} + ) records.append(record) try: executor = load_executor(executor, db, logger=logger) diff --git a/jupyter_cache/cli/commands/cmd_project.py b/jupyter_cache/cli/commands/cmd_project.py index d542939..7ebe5d8 100644 --- a/jupyter_cache/cli/commands/cmd_project.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -7,6 +7,7 @@ from jupyter_cache import get_cache from jupyter_cache.cli import arguments, options from jupyter_cache.cli.commands.cmd_main import jcache +from jupyter_cache.readers import NbReadError from jupyter_cache.utils import tabulate_project_records @@ -25,7 +26,7 @@ def add_notebooks(cache_path, nbpaths, reader): for path in nbpaths: # TODO deal with errors (print all at end? or option to ignore) click.echo("Adding: {}".format(path)) - db.add_nb_to_project(path, reader=reader) + db.add_nb_to_project(path, read_data={"name": reader, "type": "plugin"}) click.secho("Success!", fg="green") @@ -37,7 +38,9 @@ def add_notebooks(cache_path, nbpaths, reader): def add_notebook(cache_path, nbpath, reader, asset_paths): """Add notebook(s) to the project, with possible asset files.""" db = get_cache(cache_path) - db.add_nb_to_project(nbpath, reader=reader, assets=asset_paths) + db.add_nb_to_project( + nbpath, read_data={"name": reader, "type": "plugin"}, assets=asset_paths + ) click.secho("Success!", fg="green") @@ -119,9 +122,15 @@ def show_project_record(cache_path, pk_path, tb): except KeyError: click.secho("ID {} does not exist, Aborting!".format(pk_path), fg="red") sys.exit(1) - cache_record = db.get_cached_project_nb(record.uri) - data = record.format_dict(cache_record=cache_record, path_length=None, assets=False) - click.echo(yaml.safe_dump(data, sort_keys=False).rstrip()) + cache_record = None + try: + cache_record = db.get_cached_project_nb(record.uri) + except NbReadError as exc: + click.secho(f"File could not be read: {exc}", fg="red") + data = record.format_dict( + cache_record=cache_record, path_length=None, assets=False, read_name=False + ) + click.echo(yaml.safe_dump(data, sort_keys=False, allow_unicode=True).rstrip()) if record.assets: click.echo("Assets:") for path in record.assets: diff --git a/jupyter_cache/readers.py b/jupyter_cache/readers.py index 2fb6e28..669198f 100644 --- a/jupyter_cache/readers.py +++ b/jupyter_cache/readers.py @@ -1,16 +1,11 @@ """Module for handling different functions to read "notebook-like" files.""" -import threading -from typing import Callable, Set +from typing import Any, Callable, Dict, Set import nbformat as nbf from .entry_points import ENTRY_POINT_GROUP_READER, get_entry_point, list_group_names -# a thread safe cache for notebook read functions -# we include this in addition to entry points, since myst_nb needs to add them dynamically -_THREAD_CACHE = threading.local() -_THREAD_CACHE.readers = {} -_READERS = _THREAD_CACHE.readers +DEFAULT_READ_DATA = (("name", "nbformat"), ("type", "plugin")) def nbf_reader(uri: str) -> nbf.NotebookNode: @@ -27,36 +22,20 @@ def jupytext_reader(uri: str) -> nbf.NotebookNode: return jupytext.read(uri) -def add_reader( - key: str, - reader: Callable[[str], nbf.NotebookNode], - override: bool = False, -) -> None: - """Add a reader function to the cache. - - :param extension: The key to store the reader under. - :param reader: A function that takes a path as input and returns a notebook node. - :param override: If True, override an existing reader. - - """ - if not override and ( - key in _READERS or get_entry_point(ENTRY_POINT_GROUP_READER, key) - ): - raise ValueError(f"Reader '{key}' already exists") - - _READERS[key] = reader - - def list_readers() -> Set[str]: """List all available readers.""" - return set(list(_READERS) + list(list_group_names(ENTRY_POINT_GROUP_READER))) + return list_group_names(ENTRY_POINT_GROUP_READER) -def get_reader(key: str) -> Callable[[str], nbf.NotebookNode]: +def get_reader(data: Dict[str, Any]) -> Callable[[str], nbf.NotebookNode]: """Returns a function to read a file URI and return a notebook.""" - if key in _READERS: - return _READERS[key] - reader = get_entry_point(ENTRY_POINT_GROUP_READER, key) - if reader is not None: - return reader.load() - raise ValueError(f"No reader found for '{key}'") + if data.get("type") == "plugin": + key = data.get("name", "") + reader = get_entry_point(ENTRY_POINT_GROUP_READER, key) + if reader is not None: + return reader.load() + raise ValueError(f"No reader found for: {data!r}") + + +class NbReadError(IOError): + """Error raised when a notebook cannot be read.""" diff --git a/jupyter_cache/utils.py b/jupyter_cache/utils.py index c446499..002bf2f 100644 --- a/jupyter_cache/utils.py +++ b/jupyter_cache/utils.py @@ -2,7 +2,13 @@ import time from pathlib import Path -from typing import List, Union +from typing import TYPE_CHECKING, List, Optional, Union + +from jupyter_cache.readers import NbReadError + +if TYPE_CHECKING: + from jupyter_cache.base import JupyterCacheAbstract + from jupyter_cache.cache.db import NbCacheRecord, NbProjectRecord def to_relative_paths( @@ -64,7 +70,7 @@ def __exit__(self, *exc_info): self.split() -def shorten_path(file_path, length): +def shorten_path(file_path: Union[str, Path], length: Optional[int]) -> Path: """Split the path into separate parts, select the last 'length' elements and join them again """ @@ -73,7 +79,9 @@ def shorten_path(file_path, length): return Path(*Path(file_path).parts[-length:]) -def tabulate_cache_records(records: list, hashkeys=False, path_length=None) -> str: +def tabulate_cache_records( + records: List["NbCacheRecord"], hashkeys=False, path_length=None +) -> str: """Tabulate cache records. :param records: list of ``NbCacheRecord`` @@ -92,7 +100,10 @@ def tabulate_cache_records(records: list, hashkeys=False, path_length=None) -> s def tabulate_project_records( - records: list, path_length=None, cache=None, assets=False + records: List["NbProjectRecord"], + path_length: Optional[int] = None, + cache: Optional["JupyterCacheAbstract"] = None, + assets=False, ) -> str: """Tabulate cache records. @@ -107,11 +118,18 @@ def tabulate_project_records( rows = [] for record in records: cache_record = None + read_error = None if cache is not None: - cache_record = cache.get_cached_project_nb(record.uri) + try: + cache_record = cache.get_cached_project_nb(record.uri) + except NbReadError as exc: + read_error = f"{exc.__class__.__name__}: {exc}" rows.append( record.format_dict( - cache_record=cache_record, path_length=path_length, assets=assets + cache_record=cache_record, + path_length=path_length, + assets=assets, + read_error=read_error, ) ) return tabulate.tabulate(rows, headers="keys") diff --git a/tests/test_cache.py b/tests/test_cache.py index 64562f8..30cc191 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -240,7 +240,10 @@ def test_execution_jupytext(tmp_path): db = JupyterCacheBase(str(tmp_path / "cache")) temp_nb_path = tmp_path / "notebooks" shutil.copytree(NB_PATH, temp_nb_path) - db.add_nb_to_project(path=os.path.join(temp_nb_path, "basic.md"), reader="jupytext") + db.add_nb_to_project( + path=os.path.join(temp_nb_path, "basic.md"), + read_data={"name": "jupytext", "type": "plugin"}, + ) executor = load_executor("local-serial", db) result = executor.run_and_cache() print(result) From 626a38727bf7891c35ac4b626613ed90d947a144 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 12 Jan 2022 18:57:48 +0100 Subject: [PATCH 30/39] add version to setting table, add invalidate command --- docs/using/cli.md | 17 +++++++++++++++++ jupyter_cache/cache/db.py | 15 ++++++++++----- jupyter_cache/cli/commands/cmd_project.py | 20 ++++++++++++++++++++ jupyter_cache/cli/options.py | 17 +++++++++++++++++ tests/test_cli.py | 15 +++++++++++++++ tests/test_db.py | 3 ++- tox.ini | 2 ++ 7 files changed, 83 insertions(+), 6 deletions(-) diff --git a/docs/using/cli.md b/docs/using/cli.md index c73038a..617ce3d 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -78,6 +78,8 @@ or clear all notebooks from the project: :input: y ``` +## Add a custom reader to read notebook files + By default, notebook files are read using the [nbformat reader](https://nbformat.readthedocs.io/en/latest/api.html#nbformat.read). However, you can also specify a custom reader, defined by an entry point in the `jcache.readers` group. Included with jupyter_cache is the [jupytext](https://jupytext.readthedocs.io) reader, for formats like MyST Markdown: @@ -185,6 +187,21 @@ You can merge the cached outputs into a source notebook with: :args: tests/notebooks/basic.md _executed_notebook.ipynb ``` +## Invalidating cached notebooks + +If you want to invalidate a notebook's cached execution, +for example if you have changed the notebook's execution environment, +you can do so by calling the `invalidate` command: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: invalidate +:args: tests/notebooks/basic.ipynb +``` + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: list +``` + ## Specifying notebooks with assets When executing in a temporary directory, you may want to specify additional "asset" files that also need to be be copied to this directory for the notebook to run. diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 93a1c3f..7271856 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from datetime import datetime from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from sqlalchemy import JSON, Column, DateTime, Integer, String, Text from sqlalchemy.engine import Engine, create_engine @@ -11,20 +11,25 @@ from sqlalchemy.orm import Session, sessionmaker, validates from sqlalchemy.sql.expression import desc +from jupyter_cache import __version__ from jupyter_cache.utils import shorten_path OrmBase = declarative_base() -# TODO store this in the database so we can check for updates DB_VERSION = 2 -# v2: +# 0.5.0: # - table: nbstage -> nbproject # - added read_data field to nbproject -def create_db(path, name="global.db") -> Engine: +def create_db(path: Union[str, Path], name="global.db") -> Engine: + """Get or create a database at the given path.""" + exists = (Path(path) / name).exists() engine = create_engine("sqlite:///{}".format(os.path.join(path, name))) - OrmBase.metadata.create_all(engine) + if not exists: + OrmBase.metadata.create_all(engine) + Setting.set_value("__version__", __version__, engine) + return engine diff --git a/jupyter_cache/cli/commands/cmd_project.py b/jupyter_cache/cli/commands/cmd_project.py index 7ebe5d8..6587cfd 100644 --- a/jupyter_cache/cli/commands/cmd_project.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -74,6 +74,26 @@ def remove_nbs(cache_path, pk_paths): click.secho("Success!", fg="green") +@cmnd_project.command("invalidate") +@arguments.PK_OR_PATHS +@options.INVALIDATE_ALL +@options.CACHE_PATH +def invalidate_nbs(cache_path, pk_paths, invalidate_all): + """Invalidate notebook cache(s) (by ID/URI).""" + db = get_cache(cache_path) + if invalidate_all: + pk_paths = [str(record.pk) for record in db.list_project_records()] + for pk_path in pk_paths: + # TODO deal with errors (print all at end? or option to ignore) + click.echo("Invalidating: {}".format(pk_path)) + record = db.get_cached_project_nb( + int(pk_path) if pk_path.isdigit() else os.path.abspath(pk_path) + ) + if record is not None: + db.remove_cache(record.pk) + click.secho("Success!", fg="green") + + @cmnd_project.command("list") @options.CACHE_PATH # @click.option( diff --git a/jupyter_cache/cli/options.py b/jupyter_cache/cli/options.py index df8d75b..6598b61 100644 --- a/jupyter_cache/cli/options.py +++ b/jupyter_cache/cli/options.py @@ -146,6 +146,23 @@ def confirm_remove_all(ctx, param, remove_all): ) +def confirm_invalidate_all(ctx, param, remove_all): + if remove_all and not click.confirm("Are you sure you want to invalidate all?"): + click.secho("Aborted!", bold=True, fg="red") + ctx.exit() + return remove_all + + +INVALIDATE_ALL = click.option( + "-a", + "--all", + "invalidate_all", + is_flag=True, + help="Invalidate all notebooks.", + callback=confirm_invalidate_all, +) + + def set_log_level(logger): """Set the log level of the logger.""" diff --git a/tests/test_cli.py b/tests/test_cli.py index 9c26c69..a48b803 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -240,3 +240,18 @@ def test_project_merge(tmp_path): assert result.exception is None, result.output assert result.exit_code == 0, result.output assert (tmp_path / "output.ipynb").exists() + + +def test_project_invalidate(tmp_path): + db = JupyterCacheBase(str(tmp_path)) + db.cache_notebook_file( + path=os.path.join(NB_PATH, "basic.ipynb"), check_validity=False + ) + db.add_nb_to_project(path=os.path.join(NB_PATH, "basic.ipynb")) + + runner = CliRunner() + result = runner.invoke(cmd_project.invalidate_nbs, ["-p", tmp_path, "1"]) + assert result.exception is None, result.output + assert result.exit_code == 0, result.output + assert db.list_project_records() + assert not db.list_cache_records() diff --git a/tests/test_db.py b/tests/test_db.py index a5a0101..4789e31 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,5 +1,6 @@ import pytest +from jupyter_cache import __version__ from jupyter_cache.cache.db import NbCacheRecord, Setting, create_db @@ -7,7 +8,7 @@ def test_setting(tmp_path): db = create_db(tmp_path) Setting.set_value("a", 1, db) assert Setting.get_value("a", db) == 1 - assert Setting.get_dict(db) == {"a": 1} + assert Setting.get_dict(db) == {"a": 1, "__version__": __version__} def test_nb_record(tmp_path): diff --git a/tox.ini b/tox.ini index 4b101bb..dfbd14a 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,8 @@ extras = testing deps = black flake8 +setenv = + SQLALCHEMY_WARN_20 = 1 commands = pytest {posargs} [testenv:cli] From add3a92257de3c2126edc88ea85b2b5bfaf2a0ac Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 13 Jan 2022 02:30:38 +0100 Subject: [PATCH 31/39] `jcache project` -> `jcache notebook` --- .gitignore | 1 + docs/conf.py | 6 +++ docs/index.md | 12 +++--- docs/using/cli.md | 42 +++++++++---------- jupyter_cache/cache/db.py | 1 + jupyter_cache/cli/commands/__init__.py | 2 +- .../{cmd_project.py => cmd_notebook.py} | 22 +++++----- tests/test_cli.py | 20 ++++----- 8 files changed, 57 insertions(+), 49 deletions(-) rename jupyter_cache/cli/commands/{cmd_project.py => cmd_notebook.py} (93%) diff --git a/.gitignore b/.gitignore index 56e666a..7bbaf03 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,4 @@ _archive/ .vscode/ ~$* _*.ipynb +final_notebook.ipynb diff --git a/docs/conf.py b/docs/conf.py index c0f0762..c9617d5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -95,6 +95,8 @@ def setup(app): from sphinx.util.docutils import SphinxDirective class JcacheClear(SphinxDirective): + """A directive to clear the jupyter cache.""" + def run(self): path = os.path.join(os.path.dirname(self.env.app.srcdir), ".jupyter_cache") if os.path.exists(path): @@ -102,6 +104,10 @@ def run(self): return [] class JcacheCli(SphinxDirective): + """A directive to run a CLI command, + and output a nicely formatted representation of the input command and its output. + """ + required_arguments = 1 # command final_argument_whitespace = False has_content = False diff --git a/docs/index.md b/docs/index.md index 69daa84..38b55a9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,7 +33,7 @@ conda install jupyter-cache Add one or more source notebook files to the "project" (a folder containing a database and a cache of executed notebooks): -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +```{jcache-cli} jupyter_cache.cli.commands.cmd_notebook:cmnd_notebook :command: add :args: tests/notebooks/basic_unrun.ipynb tests/notebooks/basic_failing.ipynb :input: y @@ -41,7 +41,7 @@ Add one or more source notebook files to the "project" (a folder containing a da These files are now ready for execution: -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +```{jcache-cli} jupyter_cache.cli.commands.cmd_notebook:cmnd_notebook :command: list ``` @@ -53,7 +53,7 @@ Now run the execution: Successfully executed files will now be associated with a record in the cache: -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +```{jcache-cli} jupyter_cache.cli.commands.cmd_notebook:cmnd_notebook :command: list ``` @@ -71,11 +71,11 @@ Next time we execute, jupyter-cache will check which files require re-execution: ``` The source files themselves will not be modified during/after execution. -You can merge the cached outputs into a source notebook with: +You can create a new "final" notebook, with the cached outputs merged into the source notebook with: -```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +```{jcache-cli} jupyter_cache.cli.commands.cmd_notebook:cmnd_notebook :command: merge -:args: 1 _executed_notebook.ipynb +:args: 1 final_notebook.ipynb ``` ## Design considerations diff --git a/docs/using/cli.md b/docs/using/cli.md index 617ce3d..0f64c7f 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -13,7 +13,7 @@ Note, you can follow this tutorial by cloning Date: Thu, 13 Jan 2022 05:46:56 +0100 Subject: [PATCH 32/39] Move `config`,`clear` and `execute` to `jcache project`, add version to caches, move `--cache-path` to sub-levels --- docs/index.md | 17 ++- docs/using/cli.md | 19 ++- jupyter_cache/base.py | 4 + jupyter_cache/cache/db.py | 25 +++- jupyter_cache/cache/main.py | 5 +- jupyter_cache/cli/__init__.py | 45 ++++++ jupyter_cache/cli/commands/__init__.py | 3 +- jupyter_cache/cli/commands/cmd_cache.py | 73 +++++---- jupyter_cache/cli/commands/cmd_config.py | 21 --- jupyter_cache/cli/commands/cmd_main.py | 16 -- jupyter_cache/cli/commands/cmd_notebook.py | 67 ++++----- .../commands/{cmd_exec.py => cmd_project.py} | 65 +++++++- jupyter_cache/cli/options.py | 16 +- tests/test_cache.py | 8 + tests/test_cli.py | 139 +++++++++--------- tests/test_db.py | 3 +- 16 files changed, 309 insertions(+), 217 deletions(-) delete mode 100644 jupyter_cache/cli/commands/cmd_config.py rename jupyter_cache/cli/commands/{cmd_exec.py => cmd_project.py} (57%) diff --git a/docs/index.md b/docs/index.md index 38b55a9..ab5d54b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,7 +47,7 @@ These files are now ready for execution: Now run the execution: -```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: execute ``` @@ -60,13 +60,13 @@ Successfully executed files will now be associated with a record in the cache: The cache record includes execution statistics: ```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: show +:command: info :args: 1 ``` Next time we execute, jupyter-cache will check which files require re-execution: -```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: execute ``` @@ -78,6 +78,17 @@ You can create a new "final" notebook, with the cached outputs merged into the s :args: 1 final_notebook.ipynb ``` +You can also add notebooks with custom formats, such as those read by [jupytext](https://jupytext.readthedocs.io): + +```{jcache-cli} jupyter_cache.cli.commands.cmd_notebook:cmnd_notebook +:command: add +:args: --reader jupytext tests/notebooks/basic.md +``` + +```{jcache-cli} jupyter_cache.cli.commands.cmd_notebook:cmnd_notebook +:command: list +``` + ## Design considerations Although there are certainly other use cases, the principle use case this was written for is generating books / websites, created from multiple notebooks (and other text documents). diff --git a/docs/using/cli.md b/docs/using/cli.md index 0f64c7f..e1746a6 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -2,11 +2,11 @@ # Command-Line +Note, you can follow this tutorial by cloning , and running these commands inside it.: +tox ```{jcache-clear} ``` -Note, you can follow this tutorial by cloning : - ```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache :args: --help ``` @@ -18,9 +18,12 @@ The first time the cache is required, it will be lazily created: :input: y ``` +You can specify the path to the cache, with the `--cache-path` option, +or set the `JUPYTERCACHE` environment variable. + You can also clear it at any time: -```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: clear :input: y ``` @@ -114,7 +117,7 @@ jupyter-cache includes these executors: - `temp-serial`: execute notebooks with a temporary working directory, in serial mode (using a single process). - `temp-parallel`: execute notebooks with a temporary working directory, in parallel mode (using multiple processes). -```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: execute :args: --executor local-serial ``` @@ -135,7 +138,7 @@ Note here both notebooks share the same cached notebook (denoted by `[1]` in the Next time you execute the project, only notebooks which don't match a cached record will be executed: -```{jcache-cli} jupyter_cache.cli.commands.cmd_main:jcache +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project :command: execute :args: --executor local-serial -v CRITICAL ``` @@ -160,14 +163,14 @@ change this default with `jcache config cache-limit` You can see the elapsed execution time of a notebook via its ID in the cache: ```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache -:command: show +:command: info :args: 1 ``` Failed execution tracebacks are also available on the project record: ```{jcache-cli} jupyter_cache.cli.commands.cmd_notebook:cmnd_notebook -:command: show +:command: info :args: --tb tests/notebooks/basic_failing.ipynb ``` @@ -217,7 +220,7 @@ When executing in a temporary directory, you may want to specify additional "ass ``` ```{jcache-cli} jupyter_cache.cli.commands.cmd_notebook:cmnd_notebook -:command: show +:command: info :args: tests/notebooks/basic.ipynb ``` diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index 563da1a..50065fa 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -135,6 +135,10 @@ class JupyterCacheAbstract(ABC): Note: class instances should be pickleable. """ + @abstractmethod + def get_version(self) -> Optional[str]: + """Return the version of the cache.""" + @abstractmethod def clear_cache(self) -> None: """Clear the cache completely.""" diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 1fff569..f4ab4f9 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -15,24 +15,37 @@ from jupyter_cache.utils import shorten_path OrmBase = declarative_base() +DB_NAME = "global.db" -DB_VERSION = 2 +# version changes: # 0.5.0: +# - __version__ key added to settings table on creation # - table: nbstage -> nbproject # - added read_data field to nbproject -def create_db(path: Union[str, Path], name="global.db") -> Engine: - """Get or create a database at the given path.""" - exists = (Path(path) / name).exists() - engine = create_engine("sqlite:///{}".format(os.path.join(path, name))) +def create_db(path: Union[str, Path]) -> Engine: + """Get or create a database at the given path. + + :param path: The path to the cache folder. + """ + exists = (Path(path) / DB_NAME).exists() + engine = create_engine("sqlite:///{}".format(os.path.join(path, DB_NAME))) if not exists: + # add all the tables, and a version identifier OrmBase.metadata.create_all(engine) - Setting.set_value("__version__", __version__, engine) + Path(path).joinpath("__version__.txt").write_text(__version__) return engine +def get_version(path: Union[str, Path]) -> Optional[str]: + """Attempt to get the version of the cache.""" + version_file = Path(path).joinpath("__version__.txt") + if version_file.exists(): + return version_file.read_text().strip() + + @contextmanager def session_context(engine: Engine): """Open a connection to the database.""" diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 8cf81bf..0a5d4df 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -22,7 +22,7 @@ from jupyter_cache.readers import DEFAULT_READ_DATA, NbReadError, get_reader from jupyter_cache.utils import to_relative_paths -from .db import NbCacheRecord, NbProjectRecord, Setting, create_db +from .db import NbCacheRecord, NbProjectRecord, Setting, create_db, get_version CACHE_LIMIT_KEY = "cache_limit" DEFAULT_CACHE_LIMIT = 1000 @@ -82,6 +82,9 @@ def __getstate__(self): state["_db"] = None return state + def get_version(self) -> Optional[str]: + return get_version(self.path) + def clear_cache(self): """Clear the cache completely.""" shutil.rmtree(self.path) diff --git a/jupyter_cache/cli/__init__.py b/jupyter_cache/cli/__init__.py index e69de29..2f272da 100644 --- a/jupyter_cache/cli/__init__.py +++ b/jupyter_cache/cli/__init__.py @@ -0,0 +1,45 @@ +import os +from pathlib import Path +from typing import TYPE_CHECKING + +import click + +if TYPE_CHECKING: + from jupyter_cache.base import JupyterCacheAbstract + + +class CacheContext: + """Context for retrieving the cache.""" + + def __init__(self, cache_path=None) -> None: + if cache_path is None: + self._cache_path = os.environ.get( + "JUPYTERCACHE", os.path.join(os.getcwd(), ".jupyter_cache") + ) + else: + self._cache_path = cache_path + + @property + def cache_path(self) -> Path: + return Path(self._cache_path) + + def get_cache(self, ask_on_missing=True) -> "JupyterCacheAbstract": + """Get the cache.""" + from jupyter_cache import get_cache + + if (not self.cache_path.exists()) and ask_on_missing: + click.secho("Cache path: ", fg="green", nl=False) + click.echo(str(self.cache_path)) + if not click.confirm( + "The cache does not yet exist, do you want to create it?" + ): + raise click.Abort() + + # gets created lazily + return get_cache(self.cache_path) + + def set_cache_path(self, cache_path: str) -> None: + self._cache_path = cache_path + + +pass_cache = click.make_pass_decorator(CacheContext, ensure=True) diff --git a/jupyter_cache/cli/commands/__init__.py b/jupyter_cache/cli/commands/__init__.py index a1f1d4c..87bd10e 100644 --- a/jupyter_cache/cli/commands/__init__.py +++ b/jupyter_cache/cli/commands/__init__.py @@ -1,6 +1,5 @@ """The jupyter-cache CLI.""" from .cmd_cache import * # noqa: F401,F403,E402 -from .cmd_config import * # noqa: F401,F403,E402 -from .cmd_exec import * # noqa: F401,F403,E402 from .cmd_notebook import * # noqa: F401,F403,E402 +from .cmd_project import * # noqa: F401,F403,E402 diff --git a/jupyter_cache/cli/commands/cmd_cache.py b/jupyter_cache/cli/commands/cmd_cache.py index e50b9fd..5427caf 100644 --- a/jupyter_cache/cli/commands/cmd_cache.py +++ b/jupyter_cache/cli/commands/cmd_cache.py @@ -1,21 +1,19 @@ -import sys - import click -from jupyter_cache import get_cache -from jupyter_cache.cli import arguments, options +from jupyter_cache.cli import arguments, options, pass_cache from jupyter_cache.cli.commands.cmd_main import jcache from jupyter_cache.utils import tabulate_cache_records @jcache.group("cache") -def cmnd_cache(): - """Commands for interacting with cached executions.""" - pass +@options.CACHE_PATH +@pass_cache +def cmnd_cache(cache, cache_path): + """Work with cached execution(s) in a project.""" + cache.set_cache_path(cache_path) @cmnd_cache.command("list") -@options.CACHE_PATH @click.option( "-l", "--latest-only", @@ -24,9 +22,10 @@ def cmnd_cache(): ) @click.option("-h", "--hashkeys", is_flag=True, help="Show the hashkey of notebook.") @options.PATH_LENGTH -def list_caches(cache_path, latest_only, hashkeys, path_length): - """List cached notebook records in the cache.""" - db = get_cache(cache_path) +@pass_cache +def list_caches(cache, latest_only, hashkeys, path_length): + """List cached notebook records.""" + db = cache.get_cache() records = db.list_cache_records() if not records: click.secho("No Cached Notebooks", fg="blue") @@ -45,19 +44,19 @@ def list_caches(cache_path, latest_only, hashkeys, path_length): ) -@cmnd_cache.command("show") -@options.CACHE_PATH +@cmnd_cache.command("info") @arguments.PK -def show_cache(cache_path, pk): - """Show details of a cached notebook in the cache.""" +@pass_cache +def cached_info(cache, pk): + """Show details of a cached notebook.""" import yaml - db = get_cache(cache_path) + db = cache.get_cache() try: record = db.get_cache_record(pk) except KeyError: click.secho("ID {} does not exist, Aborting!".format(pk), fg="red") - sys.exit(1) + raise click.Abort() data = record.format_dict(hashkey=True, path_length=None) click.echo(yaml.safe_dump(data, sort_keys=False), nl=False) with db.cache_artefacts_temppath(pk) as folder: @@ -72,20 +71,20 @@ def show_cache(cache_path, pk): @cmnd_cache.command("cat-artefact") -@options.CACHE_PATH @arguments.PK @arguments.ARTIFACT_RPATH -def cat_artifact(cache_path, pk, artifact_rpath): +@pass_cache +def cat_artifact(cache, pk, artifact_rpath): """Print the contents of a cached artefact.""" - db = get_cache(cache_path) + db = cache.get_cache() with db.cache_artefacts_temppath(pk) as path: artifact_path = path.joinpath(artifact_rpath) if not artifact_path.exists(): click.secho("Artifact does not exist", fg="red") - sys.exit(1) + raise click.Abort() if not artifact_path.is_file(): click.secho("Artifact is not a file", fg="red") - sys.exit(1) + raise click.Abort() text = artifact_path.read_text(encoding="utf8") click.echo(text) @@ -127,12 +126,12 @@ def cache_file(db, nbpath, validate, overwrite, artifact_paths=()): @cmnd_cache.command("add-with-artefacts") @arguments.ARTIFACT_PATHS @options.NB_PATH -@options.CACHE_PATH @options.VALIDATE_NB @options.OVERWRITE_CACHED -def cache_nb(cache_path, artifact_paths, nbpath, validate, overwrite): +@pass_cache +def cache_nb(cache, artifact_paths, nbpath, validate, overwrite): """Cache a notebook, with possible artefact files.""" - db = get_cache(cache_path) + db = cache.get_cache() success = cache_file(db, nbpath, validate, overwrite, artifact_paths) if success: click.secho("Success!", fg="green") @@ -140,12 +139,12 @@ def cache_nb(cache_path, artifact_paths, nbpath, validate, overwrite): @cmnd_cache.command("add") @arguments.NB_PATHS -@options.CACHE_PATH @options.VALIDATE_NB @options.OVERWRITE_CACHED -def cache_nbs(cache_path, nbpaths, validate, overwrite): +@pass_cache +def cache_nbs(cache, nbpaths, validate, overwrite): """Cache notebook(s) that have already been executed.""" - db = get_cache(cache_path) + db = cache.get_cache() success = True for nbpath in nbpaths: # TODO deal with errors (print all at end? or option to ignore) @@ -156,11 +155,11 @@ def cache_nbs(cache_path, nbpaths, validate, overwrite): @cmnd_cache.command("clear") -@options.CACHE_PATH @options.FORCE -def clear_cache_cmd(cache_path, force): +@pass_cache +def clear_cache_cmd(cache, force): """Remove all executed notebooks from the cache.""" - db = get_cache(cache_path) + db = cache.get_cache() if not force: click.confirm( "Are you sure you want to permanently clear the cache!?", abort=True @@ -172,13 +171,13 @@ def clear_cache_cmd(cache_path, force): @cmnd_cache.command("remove") @arguments.PKS -@options.CACHE_PATH @options.REMOVE_ALL -def remove_caches(cache_path, pks, remove_all): +@pass_cache +def remove_caches(cache, pks, remove_all): """Remove notebooks stored in the cache.""" from jupyter_cache.base import CachingError - db = get_cache(cache_path) + db = cache.get_cache() if remove_all: pks = [r.pk for r in db.list_cache_records()] for pk in pks: @@ -197,9 +196,9 @@ def remove_caches(cache_path, pks, remove_all): @cmnd_cache.command("diff") @arguments.PK @arguments.NB_PATH -@options.CACHE_PATH -def diff_nb(cache_path, pk, nbpath): +@pass_cache +def diff_nb(cache, pk, nbpath): """Print a diff of a notebook to one stored in the cache.""" - db = get_cache(cache_path) + db = cache.get_cache() click.echo(db.diff_nbfile_with_cache(pk, nbpath, as_str=True)) click.secho("Success!", fg="green") diff --git a/jupyter_cache/cli/commands/cmd_config.py b/jupyter_cache/cli/commands/cmd_config.py deleted file mode 100644 index dfea25b..0000000 --- a/jupyter_cache/cli/commands/cmd_config.py +++ /dev/null @@ -1,21 +0,0 @@ -import click - -from jupyter_cache import get_cache -from jupyter_cache.cli import options -from jupyter_cache.cli.commands.cmd_main import jcache - - -@jcache.group("config") -def cmnd_config(): - """Commands for configuring the cache.""" - pass - - -@cmnd_config.command("cache-limit") -@options.CACHE_PATH -@click.argument("limit", metavar="CACHE_LIMIT", type=int) -def change_cache_limit(cache_path, limit): - """Change the maximum number of notebooks stored in the cache.""" - db = get_cache(cache_path) - db.change_cache_limit(limit) - click.secho("Cache limit changed!", fg="green") diff --git a/jupyter_cache/cli/commands/cmd_main.py b/jupyter_cache/cli/commands/cmd_main.py index 091e859..1581902 100644 --- a/jupyter_cache/cli/commands/cmd_main.py +++ b/jupyter_cache/cli/commands/cmd_main.py @@ -12,19 +12,3 @@ @options.AUTOCOMPLETE def jcache(*args, **kwargs): """The command line interface of jupyter-cache.""" - - -@jcache.command("clear") -@options.CACHE_PATH -@options.FORCE -def clear_cache(cache_path, force): - """Clear the cache completely.""" - from jupyter_cache.cache.main import JupyterCacheBase - - db = JupyterCacheBase(cache_path) - if not force: - click.confirm( - "Are you sure you want to permanently clear the cache!?", abort=True - ) - db.clear_cache() - click.secho("Cache cleared!", fg="green") diff --git a/jupyter_cache/cli/commands/cmd_notebook.py b/jupyter_cache/cli/commands/cmd_notebook.py index 1f75a99..b411198 100644 --- a/jupyter_cache/cli/commands/cmd_notebook.py +++ b/jupyter_cache/cli/commands/cmd_notebook.py @@ -1,28 +1,29 @@ import os -import sys import click import nbformat -from jupyter_cache import get_cache -from jupyter_cache.cli import arguments, options +from jupyter_cache.cli import arguments, options, pass_cache from jupyter_cache.cli.commands.cmd_main import jcache from jupyter_cache.readers import NbReadError from jupyter_cache.utils import tabulate_project_records @jcache.group("notebook") -def cmnd_notebook(): - """Commands for interacting with a project.""" +@options.CACHE_PATH +@pass_cache +def cmnd_notebook(cache, cache_path): + """Work with notebook(s) in a project.""" + cache.set_cache_path(cache_path) @cmnd_notebook.command("add") @arguments.NB_PATHS @options.READER_KEY -@options.CACHE_PATH -def add_notebooks(cache_path, nbpaths, reader): +@pass_cache +def add_notebooks(cache, nbpaths, reader): """Add notebook(s) to the project.""" - db = get_cache(cache_path) + db = cache.get_cache() for path in nbpaths: # TODO deal with errors (print all at end? or option to ignore) click.echo("Adding: {}".format(path)) @@ -34,10 +35,10 @@ def add_notebooks(cache_path, nbpaths, reader): @arguments.ASSET_PATHS @options.NB_PATH @options.READER_KEY -@options.CACHE_PATH -def add_notebook(cache_path, nbpath, reader, asset_paths): +@pass_cache +def add_notebook(cache, nbpath, reader, asset_paths): """Add notebook(s) to the project, with possible asset files.""" - db = get_cache(cache_path) + db = cache.get_cache() db.add_nb_to_project( nbpath, read_data={"name": reader, "type": "plugin"}, assets=asset_paths ) @@ -45,11 +46,11 @@ def add_notebook(cache_path, nbpath, reader, asset_paths): @cmnd_notebook.command("clear") -@options.CACHE_PATH @options.FORCE -def clear_nbs(cache_path, force): +@pass_cache +def clear_nbs(cache, force): """Remove all notebooks from the project.""" - db = get_cache(cache_path) + db = cache.get_cache() if not force: click.confirm( "Are you sure you want to permanently clear the project!?", abort=True @@ -61,10 +62,10 @@ def clear_nbs(cache_path, force): @cmnd_notebook.command("remove") @arguments.PK_OR_PATHS -@options.CACHE_PATH -def remove_nbs(cache_path, pk_paths): +@pass_cache +def remove_nbs(cache, pk_paths): """Remove notebook(s) from the project (by ID/URI).""" - db = get_cache(cache_path) + db = cache.get_cache() for pk_path in pk_paths: # TODO deal with errors (print all at end? or option to ignore) click.echo("Removing: {}".format(pk_path)) @@ -77,10 +78,10 @@ def remove_nbs(cache_path, pk_paths): @cmnd_notebook.command("invalidate") @arguments.PK_OR_PATHS @options.INVALIDATE_ALL -@options.CACHE_PATH -def invalidate_nbs(cache_path, pk_paths, invalidate_all): - """Invalidate notebook(s) cache (by ID/URI).""" - db = get_cache(cache_path) +@pass_cache +def invalidate_nbs(cache, pk_paths, invalidate_all): + """Remove any matching cache of the notebook(s) (by ID/URI).""" + db = cache.get_cache() if invalidate_all: pk_paths = [str(record.pk) for record in db.list_project_records()] for pk_path in pk_paths: @@ -95,7 +96,6 @@ def invalidate_nbs(cache_path, pk_paths, invalidate_all): @cmnd_notebook.command("list") -@options.CACHE_PATH # @click.option( # "--compare/--no-compare", # default=True, @@ -108,9 +108,10 @@ def invalidate_nbs(cache_path, pk_paths, invalidate_all): is_flag=True, help="Show the number of assets associated with each notebook", ) -def list_nbs_in_project(cache_path, path_length, assets): +@pass_cache +def list_nbs_in_project(cache, path_length, assets): """List notebooks in the project.""" - db = get_cache(cache_path) + db = cache.get_cache() records = db.list_project_records() if not records: click.secho("No notebooks in project", fg="blue") @@ -121,8 +122,7 @@ def list_nbs_in_project(cache_path, path_length, assets): ) -@cmnd_notebook.command("show") -@options.CACHE_PATH +@cmnd_notebook.command("info") @arguments.PK_OR_PATH @click.option( "--tb/--no-tb", @@ -130,18 +130,19 @@ def list_nbs_in_project(cache_path, path_length, assets): show_default=True, help="Show traceback, if last execution failed.", ) -def show_project_record(cache_path, pk_path, tb): +@pass_cache +def show_project_record(cache, pk_path, tb): """Show details of a notebook (by ID).""" import yaml - db = get_cache(cache_path) + db = cache.get_cache() try: record = db.get_project_record( int(pk_path) if pk_path.isdigit() else os.path.abspath(pk_path) ) except KeyError: click.secho("ID {} does not exist, Aborting!".format(pk_path), fg="red") - sys.exit(1) + raise click.Abort() cache_record = None try: cache_record = db.get_cached_project_nb(record.uri) @@ -164,10 +165,10 @@ def show_project_record(cache_path, pk_path, tb): @cmnd_notebook.command("merge") @arguments.PK_OR_PATH @arguments.OUTPUT_PATH -@options.CACHE_PATH -def merge_executed(cache_path, pk_path, outpath): - """Write notebook merged with cached outputs (by ID/URI).""" - db = get_cache(cache_path) +@pass_cache +def merge_executed(cache, pk_path, outpath): + """Create notebook merged with cached outputs (by ID/URI).""" + db = cache.get_cache() nb = db.get_project_notebook( int(pk_path) if pk_path.isdigit() else os.path.abspath(pk_path) ).nb diff --git a/jupyter_cache/cli/commands/cmd_exec.py b/jupyter_cache/cli/commands/cmd_project.py similarity index 57% rename from jupyter_cache/cli/commands/cmd_exec.py rename to jupyter_cache/cli/commands/cmd_project.py index 67be5fd..72154c9 100644 --- a/jupyter_cache/cli/commands/cmd_exec.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -3,8 +3,7 @@ import click -from jupyter_cache import get_cache -from jupyter_cache.cli import arguments, options, utils +from jupyter_cache.cli import arguments, options, pass_cache, utils from jupyter_cache.cli.commands.cmd_main import jcache from jupyter_cache.readers import list_readers @@ -12,19 +11,73 @@ utils.setup_logger(logger) -@jcache.command("execute") +@jcache.group("project") +@options.CACHE_PATH +@pass_cache +def cmnd_project(cache, cache_path): + """Work with a project.""" + cache.set_cache_path(cache_path) + + +@cmnd_project.command("version") +@pass_cache +def version(cache): + """Print the version of the cache.""" + if not cache.cache_path.exists(): + click.secho("No cache found.", fg="red") + raise click.Abort() + version = cache.get_cache().get_version() + if version is None: + click.secho("Cache version not found", fg="red") + raise click.Abort() + click.echo(version) + + +@cmnd_project.command("clear") +@options.FORCE +@pass_cache +def clear_cache(cache, force): + """Clear the project cache completely.""" + if not cache.cache_path.exists(): + click.secho("Cache does not exist", fg="green") + raise click.Abort() + if not force: + click.echo(f"Cache path: {cache.cache_path}") + click.confirm( + "Are you sure you want to permanently clear the cache!?", + abort=True, + ) + cache.get_cache().clear_cache() + click.secho("Cache cleared!", fg="green") + + +@cmnd_project.command("cache-limit") +@click.argument("limit", metavar="CACHE_LIMIT", type=int, required=False) +@pass_cache +def change_cache_limit(cache, limit): + """Get/set maximum number of notebooks stored in the cache.""" + db = cache.get_cache() + if limit is None: + limit = db.get_cache_limit() + click.echo(f"Current cache limit: {limit}") + else: + db.change_cache_limit(limit) + click.secho("Cache limit changed!", fg="green") + + +@cmnd_project.command("execute") @arguments.PK_OR_PATHS @options.EXECUTOR_KEY @options.EXEC_TIMEOUT -@options.CACHE_PATH @options.set_log_level(logger) -def execute_nbs(cache_path, executor, pk_paths, timeout): +@pass_cache +def execute_nbs(cache, executor, pk_paths, timeout): """Execute all or specific outdated notebooks in the project.""" import yaml from jupyter_cache.executors import load_executor - db = get_cache(cache_path) + db = cache.get_cache() records = [] not_in_project = [] for pk_path in pk_paths: diff --git a/jupyter_cache/cli/options.py b/jupyter_cache/cli/options.py index 6598b61..ec21afe 100644 --- a/jupyter_cache/cli/options.py +++ b/jupyter_cache/cli/options.py @@ -38,7 +38,7 @@ def callback_print_cache_path(ctx, param, value): PRINT_CACHE_PATH = click.option( "-p", - "--cache-path", + "--print-path", help="Print the current cache path and exit.", is_flag=True, expose_value=True, @@ -47,24 +47,12 @@ def callback_print_cache_path(ctx, param, value): ) -def check_cache_exists(ctx, param, value): - if os.path.exists(value): - return value - click.secho("Cache path: ", fg="green", nl=False) - click.echo(value) - if not click.confirm("The cache does not yet exist, do you want to create it?"): - click.secho("Aborted!", bold=True, fg="red") - ctx.exit() - return value - - CACHE_PATH = click.option( "-p", "--cache-path", - help="Path to cache.", + help="Path to project cache.", default=default_cache_path, show_default=".jupyter_cache", - callback=check_cache_exists, ) diff --git a/tests/test_cache.py b/tests/test_cache.py index 30cc191..be7c0ca 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -5,12 +5,19 @@ import nbformat as nbf import pytest +from jupyter_cache import __version__ from jupyter_cache.base import NbValidityError from jupyter_cache.cache.main import JupyterCacheBase NB_PATH = os.path.join(os.path.realpath(os.path.dirname(__file__)), "notebooks") +def test_get_version(tmp_path): + cache = JupyterCacheBase(str(tmp_path)) + cache.db + assert cache.get_version() == __version__ + + def test_basic_workflow(tmp_path): cache = JupyterCacheBase(str(tmp_path)) with pytest.raises(NbValidityError): @@ -166,6 +173,7 @@ def test_artifacts(tmp_path): str(p.relative_to(tmp_path)) for p in tmp_path.glob("**/*") if p.is_file() } == { "global.db", + "__version__.txt", f"executed/{hashkey}/base.ipynb", f"executed/{hashkey}/artifacts/artifact_folder/artifact.txt", } diff --git a/tests/test_cli.py b/tests/test_cli.py index d8694fd..ede3a53 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,46 +1,62 @@ import os +from pathlib import Path +import pytest from click.testing import CliRunner from jupyter_cache.cache.main import JupyterCacheBase -from jupyter_cache.cli.commands import cmd_cache, cmd_exec, cmd_main, cmd_notebook +from jupyter_cache.cli import CacheContext +from jupyter_cache.cli.commands import cmd_cache, cmd_main, cmd_notebook, cmd_project NB_PATH = os.path.join(os.path.realpath(os.path.dirname(__file__)), "notebooks") -def test_base(): - runner = CliRunner() +class Runner(CliRunner): + def __init__(self, path) -> None: + super().__init__() + self._cache_path = path + + def create_cache(self) -> JupyterCacheBase: + return JupyterCacheBase(str(self._cache_path)) + + def invoke(self, *args, **kwargs): + return super().invoke(*args, **kwargs, obj=CacheContext(self._cache_path)) + + +@pytest.fixture() +def runner(tmp_path): + return Runner(tmp_path) + + +def test_base(runner: Runner): result = runner.invoke(cmd_main.jcache, "-v") assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "jupyter-cache version" in result.output.strip(), result.output -def test_clear_cache(tmp_path): - JupyterCacheBase(str(tmp_path)) - runner = CliRunner() - result = runner.invoke(cmd_main.clear_cache, ["-p", tmp_path], input="y") +def test_clear_cache(runner: Runner): + result = runner.invoke(cmd_project.clear_cache, input="y") assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "Cache cleared!" in result.output.strip(), result.output -def test_list_caches(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_list_caches(runner: Runner): + db = runner.create_cache() db.cache_notebook_file( path=os.path.join(NB_PATH, "basic.ipynb"), uri="basic.ipynb", check_validity=False, ) - runner = CliRunner() - result = runner.invoke(cmd_cache.list_caches, ["-p", tmp_path]) + result = runner.invoke(cmd_cache.list_caches, []) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output -def test_list_caches_latest_only(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_list_caches_latest_only(runner: Runner): + db = runner.create_cache() db.cache_notebook_file( path=os.path.join(NB_PATH, "basic.ipynb"), uri="basic.ipynb", @@ -51,68 +67,64 @@ def test_list_caches_latest_only(tmp_path): uri="basic.ipynb", check_validity=False, ) - runner = CliRunner() - result = runner.invoke(cmd_cache.list_caches, ["-p", tmp_path]) + result = runner.invoke(cmd_cache.list_caches, []) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert len(result.output.strip().splitlines()) == 4, result.output - result = runner.invoke(cmd_cache.list_caches, ["-p", tmp_path, "--latest-only"]) + result = runner.invoke(cmd_cache.list_caches, ["--latest-only"]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert len(result.output.strip().splitlines()) == 3, result.output -def test_cache_with_artifact(tmp_path): - JupyterCacheBase(str(tmp_path)) +def test_cache_with_artifact(runner: Runner): + nb_path = os.path.join(NB_PATH, "basic.ipynb") a_path = os.path.join(NB_PATH, "artifact_folder", "artifact.txt") - runner = CliRunner() result = runner.invoke( - cmd_cache.cache_nb, ["-p", tmp_path, "--no-validate", "-nb", nb_path, a_path] + cmd_cache.cache_nb, ["--no-validate", "-nb", nb_path, a_path] ) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output - result = runner.invoke(cmd_cache.show_cache, ["-p", tmp_path, "1"]) + result = runner.invoke(cmd_cache.cached_info, ["1"]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "- artifact_folder/artifact.txt" in result.output.strip(), result.output result = runner.invoke( - cmd_cache.cat_artifact, ["-p", tmp_path, "1", "artifact_folder/artifact.txt"] + cmd_cache.cat_artifact, ["1", "artifact_folder/artifact.txt"] ) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "An artifact" in result.output.strip(), result.output -def test_cache_nbs(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_cache_nbs(runner: Runner): + db = runner.create_cache() path = os.path.join(NB_PATH, "basic.ipynb") - runner = CliRunner() - result = runner.invoke(cmd_cache.cache_nbs, ["-p", tmp_path, "--no-validate", path]) + result = runner.invoke(cmd_cache.cache_nbs, ["--no-validate", path]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output assert db.list_cache_records()[0].uri == path -def test_remove_caches(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_remove_caches(runner: Runner): + db = runner.create_cache() db.cache_notebook_file( path=os.path.join(NB_PATH, "basic.ipynb"), uri="basic.ipynb", check_validity=False, ) - runner = CliRunner() - result = runner.invoke(cmd_cache.remove_caches, ["-p", tmp_path, "1"]) + result = runner.invoke(cmd_cache.remove_caches, ["1"]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "Success" in result.output.strip(), result.output assert db.list_cache_records() == [] -def test_diff_nbs(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_diff_nbs(runner: Runner): + db = runner.create_cache() path = os.path.join(NB_PATH, "basic.ipynb") path2 = os.path.join(NB_PATH, "basic_failing.ipynb") db.cache_notebook_file(path, check_validity=False) @@ -120,8 +132,7 @@ def test_diff_nbs(tmp_path): # nb_bundle.nb.cells[0].source = "# New Title" # db.stage_notebook_bundle(nb_bundle) - runner = CliRunner() - result = runner.invoke(cmd_cache.diff_nb, ["-p", tmp_path, "1", path2]) + result = runner.invoke(cmd_cache.diff_nb, ["1", path2]) assert result.exception is None, result.output assert result.exit_code == 0, result.output print(result.output.splitlines()[2:]) @@ -151,106 +162,98 @@ def test_diff_nbs(tmp_path): ] -def test_add_nbs_to_project(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_add_nbs_to_project(runner: Runner): + db = runner.create_cache() path = os.path.join(NB_PATH, "basic.ipynb") - runner = CliRunner() - result = runner.invoke(cmd_notebook.add_notebooks, ["-p", tmp_path, path]) + result = runner.invoke(cmd_notebook.add_notebooks, [path]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output assert db.list_project_records()[0].uri == path -def test_remove_nbs_from_project(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_remove_nbs_from_project(runner: Runner): + db = runner.create_cache() path = os.path.join(NB_PATH, "basic.ipynb") - runner = CliRunner() - result = runner.invoke(cmd_notebook.add_notebooks, ["-p", tmp_path, path]) - result = runner.invoke(cmd_notebook.remove_nbs, ["-p", tmp_path, path]) + result = runner.invoke(cmd_notebook.add_notebooks, [path]) + result = runner.invoke(cmd_notebook.remove_nbs, [path]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output assert db.list_project_records() == [] -def test_clear_project(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_clear_project(runner: Runner): + db = runner.create_cache() path = os.path.join(NB_PATH, "basic.ipynb") - runner = CliRunner() - result = runner.invoke(cmd_notebook.add_notebooks, ["-p", tmp_path, path]) - result = runner.invoke(cmd_notebook.clear_nbs, ["-p", tmp_path], input="y") + result = runner.invoke(cmd_notebook.add_notebooks, [path]) + result = runner.invoke(cmd_notebook.clear_nbs, [], input="y") assert result.exception is None, result.output assert result.exit_code == 0, result.output assert db.list_project_records() == [] -def test_list_nbs_in_project(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_list_nbs_in_project(runner: Runner): + db = runner.create_cache() db.cache_notebook_file( path=os.path.join(NB_PATH, "basic.ipynb"), check_validity=False ) db.add_nb_to_project(path=os.path.join(NB_PATH, "basic.ipynb")) db.add_nb_to_project(path=os.path.join(NB_PATH, "basic_failing.ipynb")) - runner = CliRunner() - result = runner.invoke(cmd_notebook.list_nbs_in_project, ["-p", tmp_path]) + result = runner.invoke(cmd_notebook.list_nbs_in_project, []) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output -def test_show_project_record(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_show_project_record(runner: Runner): + db = runner.create_cache() db.cache_notebook_file( path=os.path.join(NB_PATH, "basic.ipynb"), check_validity=False ) db.add_nb_to_project(path=os.path.join(NB_PATH, "basic.ipynb")) - runner = CliRunner() - result = runner.invoke(cmd_notebook.show_project_record, ["-p", tmp_path, "1"]) + result = runner.invoke(cmd_notebook.show_project_record, ["1"]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert "basic.ipynb" in result.output.strip(), result.output -def test_execute(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_project_execute(runner: Runner): + db = runner.create_cache() db.add_nb_to_project(path=os.path.join(NB_PATH, "basic.ipynb")) - runner = CliRunner() - result = runner.invoke(cmd_exec.execute_nbs, ["-p", tmp_path]) + result = runner.invoke(cmd_project.execute_nbs, []) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert len(db.list_cache_records()) == 1 -def test_project_merge(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_project_merge(runner: Runner, tmp_path: Path): + db = runner.create_cache() record = db.add_nb_to_project(path=os.path.join(NB_PATH, "basic_unrun.ipynb")) db.cache_notebook_file( path=os.path.join(NB_PATH, "basic.ipynb"), uri="basic.ipynb", check_validity=False, ) - runner = CliRunner() result = runner.invoke( cmd_notebook.merge_executed, - ["-p", tmp_path, str(record.pk), str(tmp_path / "output.ipynb")], + [str(record.pk), str(tmp_path / "output.ipynb")], ) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert (tmp_path / "output.ipynb").exists() -def test_project_invalidate(tmp_path): - db = JupyterCacheBase(str(tmp_path)) +def test_project_invalidate(runner: Runner): + db = runner.create_cache() db.cache_notebook_file( path=os.path.join(NB_PATH, "basic.ipynb"), check_validity=False ) db.add_nb_to_project(path=os.path.join(NB_PATH, "basic.ipynb")) - runner = CliRunner() - result = runner.invoke(cmd_notebook.invalidate_nbs, ["-p", tmp_path, "1"]) + result = runner.invoke(cmd_notebook.invalidate_nbs, ["1"]) assert result.exception is None, result.output assert result.exit_code == 0, result.output assert db.list_project_records() diff --git a/tests/test_db.py b/tests/test_db.py index 4789e31..a5a0101 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,6 +1,5 @@ import pytest -from jupyter_cache import __version__ from jupyter_cache.cache.db import NbCacheRecord, Setting, create_db @@ -8,7 +7,7 @@ def test_setting(tmp_path): db = create_db(tmp_path) Setting.set_value("a", 1, db) assert Setting.get_value("a", db) == 1 - assert Setting.get_dict(db) == {"a": 1, "__version__": __version__} + assert Setting.get_dict(db) == {"a": 1} def test_nb_record(tmp_path): From ff254198b061203a46713b95dcf8a52fb0dbf26c Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 13 Jan 2022 08:07:21 +0100 Subject: [PATCH 33/39] add `jcache project execute --force` --- docs/using/cli.md | 7 +++ jupyter_cache/base.py | 14 ++++-- jupyter_cache/cache/db.py | 6 +++ jupyter_cache/cache/main.py | 29 +++++++++--- jupyter_cache/cli/commands/cmd_notebook.py | 38 ++++++++++++++- jupyter_cache/cli/commands/cmd_project.py | 54 +++------------------- jupyter_cache/cli/options.py | 11 +++++ jupyter_cache/executors/base.py | 13 ++++-- jupyter_cache/executors/basic.py | 4 +- 9 files changed, 112 insertions(+), 64 deletions(-) diff --git a/docs/using/cli.md b/docs/using/cli.md index e1746a6..4313285 100644 --- a/docs/using/cli.md +++ b/docs/using/cli.md @@ -143,6 +143,13 @@ Next time you execute the project, only notebooks which don't match a cached rec :args: --executor local-serial -v CRITICAL ``` +You can also `force` all notebooks to be re-executed: + +```{jcache-cli} jupyter_cache.cli.commands.cmd_project:cmnd_project +:command: execute +:args: --force +``` + If you modify a code cell, the notebook will no longer match a cached notebook or, if you wish to re-execute unchanged notebook(s) (for example if the runtime environment has changed), you can remove their records from the cache (keeping the project record): ```{jcache-cli} jupyter_cache.cli.commands.cmd_cache:cmnd_cache diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index 50065fa..f17e0d3 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -297,8 +297,12 @@ def remove_nb_from_project(self, uri_or_pk: Union[int, str]): """Remove a notebook from the project.""" @abstractmethod - def list_project_records(self) -> List[NbProjectRecord]: - """Return a list of the notebook URI's in the project.""" + def list_project_records( + self, + filter_uris: Optional[List[str]] = None, + filter_pks: Optional[List[int]] = None, + ) -> List[NbProjectRecord]: + """Return a list of all notebook records in the project.""" @abstractmethod def get_project_record(self, uri_or_pk: Union[int, str]) -> NbProjectRecord: @@ -321,5 +325,9 @@ def get_cached_project_nb( """ @abstractmethod - def list_unexecuted(self) -> List[NbProjectRecord]: + def list_unexecuted( + self, + filter_uris: Optional[List[str]] = None, + filter_pks: Optional[List[int]] = None, + ) -> List[NbProjectRecord]: """List notebooks in the project, whose hash is not present in the cache.""" diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index f4ab4f9..9a76742 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -162,6 +162,12 @@ def create_record(uri: str, hashkey: str, db: Engine, **kwargs) -> "NbCacheRecor session.expunge(record) return record + def remove_record(pk: int, db: Engine): + with session_context(db) as session: # type: Session + record = session.get(NbCacheRecord, pk) + session.delete(record) + session.commit() + def remove_records(pks: List[int], db: Engine): with session_context(db) as session: # type: Session session.query(NbCacheRecord).filter(NbCacheRecord.pk.in_(pks)).delete( diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 0a5d4df..3d68f3c 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -219,9 +219,13 @@ def cache_notebook_bundle( "Notebook already exists in cache and overwrite=False." ) shutil.rmtree(path.parent) + + try: record = NbCacheRecord.record_from_hashkey(hashkey, self.db) - # TODO record should be changed rather than deleted? - NbCacheRecord.remove_records([record.pk], self.db) + except KeyError: + pass + else: + NbCacheRecord.remove_record(record.pk, self.db) record = NbCacheRecord.create_record( uri=bundle.uri, @@ -429,8 +433,17 @@ def add_nb_to_project( # TODO physically copy to cache? # TODO assets - def list_project_records(self) -> List[NbProjectRecord]: - return NbProjectRecord.records_all(self.db) + def list_project_records( + self, + filter_uris: Optional[List[str]] = None, + filter_pks: Optional[List[int]] = None, + ) -> List[NbProjectRecord]: + records = NbProjectRecord.records_all(self.db) + if filter_uris is not None: + records = [r for r in records if r.uri in filter_uris] + if filter_pks is not None: + records = [r for r in records if r.pk in filter_pks] + return records def get_project_record(self, uri_or_pk: Union[int, str]) -> NbProjectRecord: if isinstance(uri_or_pk, int): @@ -476,9 +489,13 @@ def get_cached_project_nb( except KeyError: return None - def list_unexecuted(self) -> List[NbProjectRecord]: + def list_unexecuted( + self, + filter_uris: Optional[List[str]] = None, + filter_pks: Optional[List[int]] = None, + ) -> List[NbProjectRecord]: records = [] - for record in self.list_project_records(): + for record in self.list_project_records(filter_uris, filter_pks): nb = self.get_project_notebook(record.uri).nb _, hashkey = self.create_hashed_notebook(nb) try: diff --git a/jupyter_cache/cli/commands/cmd_notebook.py b/jupyter_cache/cli/commands/cmd_notebook.py index b411198..13c6d4a 100644 --- a/jupyter_cache/cli/commands/cmd_notebook.py +++ b/jupyter_cache/cli/commands/cmd_notebook.py @@ -1,13 +1,17 @@ +import logging import os import click import nbformat -from jupyter_cache.cli import arguments, options, pass_cache +from jupyter_cache.cli import arguments, options, pass_cache, utils from jupyter_cache.cli.commands.cmd_main import jcache from jupyter_cache.readers import NbReadError from jupyter_cache.utils import tabulate_project_records +logger = logging.getLogger(__name__) +utils.setup_logger(logger) + @jcache.group("notebook") @options.CACHE_PATH @@ -176,3 +180,35 @@ def merge_executed(cache, pk_path, outpath): nbformat.write(nb, outpath) click.echo(f"Merged with cache PK {cached_pk}") click.secho("Success!", fg="green") + + +@cmnd_notebook.command("execute") +@arguments.PK_OR_PATHS +@options.EXECUTOR_KEY +@options.EXEC_TIMEOUT +@options.EXEC_FORCE(default=True) +@options.set_log_level(logger) +@pass_cache +def execute_nbs(cache, pk_paths, executor, timeout, force): + """Execute specific notebooks in the project.""" + import yaml + + from jupyter_cache.executors import load_executor + + uris = [os.path.abspath(p) for p in pk_paths if not p.isdigit()] or None + pks = [int(p) for p in pk_paths if p.isdigit()] or None + + db = cache.get_cache() + + try: + executor = load_executor(executor, db, logger=logger) + except ImportError as error: + logger.error(str(error)) + return 1 + result = executor.run_and_cache( + filter_pks=pks, filter_uris=uris, timeout=timeout, force=force + ) + click.secho( + "Finished! Successfully executed notebooks have been cached.", fg="green" + ) + click.echo(yaml.safe_dump(result.as_json(), sort_keys=False)) diff --git a/jupyter_cache/cli/commands/cmd_project.py b/jupyter_cache/cli/commands/cmd_project.py index 72154c9..f2e9e62 100644 --- a/jupyter_cache/cli/commands/cmd_project.py +++ b/jupyter_cache/cli/commands/cmd_project.py @@ -1,11 +1,9 @@ import logging -from pathlib import Path import click -from jupyter_cache.cli import arguments, options, pass_cache, utils +from jupyter_cache.cli import options, pass_cache, utils from jupyter_cache.cli.commands.cmd_main import jcache -from jupyter_cache.readers import list_readers logger = logging.getLogger(__name__) utils.setup_logger(logger) @@ -66,65 +64,25 @@ def change_cache_limit(cache, limit): @cmnd_project.command("execute") -@arguments.PK_OR_PATHS @options.EXECUTOR_KEY @options.EXEC_TIMEOUT +@options.EXEC_FORCE(default=False) @options.set_log_level(logger) @pass_cache -def execute_nbs(cache, executor, pk_paths, timeout): - """Execute all or specific outdated notebooks in the project.""" +def execute_nbs(cache, executor, timeout, force): + """Execute all outdated notebooks in the project.""" import yaml from jupyter_cache.executors import load_executor db = cache.get_cache() - records = [] - not_in_project = [] - for pk_path in pk_paths: - if pk_path.isdigit(): - pk_path = int(pk_path) - record = db.get_project_record(int(pk_path)) - records.append(record) - else: - try: - record = db.get_project_record(str(Path(pk_path).absolute())) - records.append(record) - except KeyError: - if not Path(pk_path).exists(): - raise FileNotFoundError(f"'{pk_path}' does not exist.") - not_in_project.append(pk_path) - if not_in_project: - not_in_project_string = "\n - ".join(not_in_project) - click.echo(f"Notebooks not in project:\n - {not_in_project_string}") - if not click.confirm("Continue (adding these files to the project)?"): - click.secho("Aborted!", bold=True, fg="red") - raise SystemExit(1) - reader = click.prompt( - "Enter the notebook reader to use", - type=click.Choice(list_readers()), - show_choices=True, - default="nbformat", - show_default=True, - ) - for pk_path in not_in_project: - record = db.add_nb_to_project( - pk_path, read_data={"name": reader, "type": "plugin"} - ) - records.append(record) try: executor = load_executor(executor, db, logger=logger) except ImportError as error: logger.error(str(error)) return 1 - result = executor.run_and_cache( - filter_pks=[record.pk for record in records] or None, timeout=timeout - ) + result = executor.run_and_cache(timeout=timeout, force=force) click.secho( "Finished! Successfully executed notebooks have been cached.", fg="green" ) - output = result.as_json() - if records: - output["up-to-date"] = list( - {record.uri for record in records}.difference(result.all()) - ) - click.echo(yaml.safe_dump(output, sort_keys=False)) + click.echo(yaml.safe_dump(result.as_json(), sort_keys=False)) diff --git a/jupyter_cache/cli/options.py b/jupyter_cache/cli/options.py index ec21afe..ff439bf 100644 --- a/jupyter_cache/cli/options.py +++ b/jupyter_cache/cli/options.py @@ -92,6 +92,17 @@ def callback_print_cache_path(ctx, param, value): ) +def EXEC_FORCE(default=False): + return click.option( + "-f", + "--force/--no-force", + help="Execute a notebook even if it is cached.", + is_flag=True, + default=default, + show_default=True, + ) + + PATH_LENGTH = click.option( "-l", "--path-length", default=3, show_default=True, help="Maximum URI path." ) diff --git a/jupyter_cache/executors/base.py b/jupyter_cache/executors/base.py index 3f2115e..260713b 100644 --- a/jupyter_cache/executors/base.py +++ b/jupyter_cache/executors/base.py @@ -66,16 +66,16 @@ def get_records( filter_uris: Optional[List[str]] = None, filter_pks: Optional[List[int]] = None, clear_tracebacks: bool = True, + force: bool = False, ) -> List[NbProjectRecord]: """Return records to execute. :param clear_tracebacks: Remove any tracebacks from previous executions """ - execute_records = self.cache.list_unexecuted() - if filter_uris is not None: - execute_records = [r for r in execute_records if r.uri in filter_uris] - if filter_pks is not None: - execute_records = [r for r in execute_records if r.pk in filter_pks] + if force: + execute_records = self.cache.list_project_records(filter_uris, filter_pks) + else: + execute_records = self.cache.list_unexecuted(filter_uris, filter_pks) if clear_tracebacks: NbProjectRecord.remove_tracebacks( [r.pk for r in execute_records], self.cache.db @@ -90,6 +90,7 @@ def run_and_cache( filter_pks: Optional[List[int]] = None, timeout: Optional[int] = 30, allow_errors: bool = False, + force: bool = False, **kwargs: Any ) -> ExecutorRunResult: """Run execution, cache successfully executed notebooks and return their URIs @@ -99,6 +100,8 @@ def run_and_cache( :param timeout: Maximum time in seconds to wait for a single cell to run for :param allow_errors: Whether to halt execution on the first cell exception (provided the cell is not tagged as an expected exception) + :param force: Whether to force execution of all notebooks, even if they are cached + :param kwargs: Additional keyword arguments to pass to the executor """ diff --git a/jupyter_cache/executors/basic.py b/jupyter_cache/executors/basic.py index 4eec72c..c7190da 100644 --- a/jupyter_cache/executors/basic.py +++ b/jupyter_cache/executors/basic.py @@ -197,10 +197,11 @@ def run_and_cache( filter_pks=None, timeout=30, allow_errors=False, + force=False, ) -> ExecutorRunResult: # Get the notebook that require re-execution execute_records = self.get_records( - filter_uris, filter_pks, clear_tracebacks=True + filter_uris, filter_pks, clear_tracebacks=True, force=force ) self.logger.info("Executing %s notebook(s) in serial" % len(execute_records)) @@ -237,6 +238,7 @@ def run_and_cache( filter_pks=None, timeout=30, allow_errors=False, + force=False, ) -> ExecutorRunResult: # Get the notebook that require re-execution execute_records = self.get_records( From f9096e05c0c48e1a31f12bd400bff362edb4b5e0 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 13 Jan 2022 08:15:49 +0100 Subject: [PATCH 34/39] Add exec_data field to nbproject --- jupyter_cache/cache/db.py | 297 +++++++++++++++++++------------------- 1 file changed, 151 insertions(+), 146 deletions(-) diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 9a76742..9eecd22 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -19,9 +19,9 @@ # version changes: # 0.5.0: -# - __version__ key added to settings table on creation +# - __version__.txt file written to cache on creation # - table: nbstage -> nbproject -# - added read_data field to nbproject +# - added read_data and exec_data fields to nbproject def create_db(path: Union[str, Path]) -> Engine: @@ -110,148 +110,6 @@ def get_dict(db: Engine) -> dict: return {k: v for k, v in results} -class NbCacheRecord(OrmBase): - """A record of an executed notebook cache.""" - - __tablename__ = "nbcache" - - pk = Column(Integer(), primary_key=True) - hashkey = Column(String(255), nullable=False, unique=True) - uri = Column(String(255), nullable=False, unique=False) - description = Column(String(255), nullable=False, default="") - data = Column(JSON()) - """Extra data, such as the execution time.""" - created = Column(DateTime, nullable=False, default=datetime.utcnow) - accessed = Column( - DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow - ) - - def __repr__(self): - return "{0}(pk={1})".format(self.__class__.__name__, self.pk) - - def to_dict(self): - return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} - - def format_dict( - self, hashkey=False, path_length=None, show_descript=False, show_data=True - ): - data = { - "ID": self.pk, - "Origin URI": str(shorten_path(self.uri, path_length)), - "Created": self.created.isoformat(" ", "minutes"), - "Accessed": self.accessed.isoformat(" ", "minutes"), - } - if show_descript: - data["Description"] = self.description - if hashkey: - data["Hashkey"] = self.hashkey - if show_data and self.data: - data["Data"] = self.data - return data - - @staticmethod - def create_record(uri: str, hashkey: str, db: Engine, **kwargs) -> "NbCacheRecord": - with session_context(db) as session: # type: Session - record = NbCacheRecord(hashkey=hashkey, uri=uri, **kwargs) - session.add(record) - try: - session.commit() - except IntegrityError: - raise ValueError(f"hashkey already exists:{hashkey}") - session.refresh(record) - session.expunge(record) - return record - - def remove_record(pk: int, db: Engine): - with session_context(db) as session: # type: Session - record = session.get(NbCacheRecord, pk) - session.delete(record) - session.commit() - - def remove_records(pks: List[int], db: Engine): - with session_context(db) as session: # type: Session - session.query(NbCacheRecord).filter(NbCacheRecord.pk.in_(pks)).delete( - synchronize_session=False - ) - session.commit() - - @staticmethod - def record_from_hashkey(hashkey: str, db: Engine) -> "NbCacheRecord": - with session_context(db) as session: # type: Session - result = ( - session.query(NbCacheRecord).filter_by(hashkey=hashkey).one_or_none() - ) - if result is None: - raise KeyError( - "Cache record not found for NB with hashkey: {}".format(hashkey) - ) - session.expunge(result) - return result - - @staticmethod - def record_from_pk(pk: int, db: Engine) -> "NbCacheRecord": - with session_context(db) as session: # type: Session - result = session.query(NbCacheRecord).filter_by(pk=pk).one_or_none() - if result is None: - raise KeyError("Cache record not found for NB with PK: {}".format(pk)) - session.expunge(result) - return result - - def touch(pk, db: Engine): - """Touch a record, to change its last accessed time.""" - with session_context(db) as session: # type: Session - record = session.query(NbCacheRecord).filter_by(pk=pk).one_or_none() - if record is None: - raise KeyError("Cache record not found for NB with PK: {}".format(pk)) - record.accessed = datetime.utcnow() - session.commit() - - def touch_hashkey(hashkey, db: Engine): - """Touch a record, to change its last accessed time.""" - with session_context(db) as session: # type: Session - record = ( - session.query(NbCacheRecord).filter_by(hashkey=hashkey).one_or_none() - ) - if record is None: - raise KeyError( - "Cache record not found for NB with hashkey: {}".format(hashkey) - ) - record.accessed = datetime.utcnow() - session.commit() - - @staticmethod - def records_from_uri(uri: str, db: Engine) -> "NbCacheRecord": - with session_context(db) as session: # type: Session - results = session.query(NbCacheRecord).filter_by(uri=uri).all() - session.expunge_all() - return results - - @staticmethod - def records_all(db: Engine) -> "NbCacheRecord": - with session_context(db) as session: # type: Session - results = session.query(NbCacheRecord).all() - session.expunge_all() - return results - - def records_to_delete(keep: int, db: Engine) -> List[int]: - """Return pks of the oldest records, where keep is number to keep.""" - with session_context(db) as session: # type: Session - pks_to_keep = [ - pk - for pk, in session.query(NbCacheRecord.pk) - .order_by(desc("accessed")) - .limit(keep) - .all() - ] - pks_to_delete = [ - pk - for pk, in session.query(NbCacheRecord.pk) - .filter(NbCacheRecord.pk.notin_(pks_to_keep)) - .all() - ] - return pks_to_delete - - class NbProjectRecord(OrmBase): """A record of a notebook within the project.""" @@ -260,9 +118,14 @@ class NbProjectRecord(OrmBase): pk = Column(Integer(), primary_key=True) uri = Column(String(255), nullable=False, unique=True) read_data = Column(JSON(), nullable=False) + """Data on how to read the uri to a notebook.""" assets = Column(JSON(), nullable=False, default=list) - traceback = Column(Text(), nullable=True, default="") + """A list of file assets required for the notebook to run.""" + exec_data = Column(JSON(), nullable=True) + """Data on how to execute the notebook.""" created = Column(DateTime, nullable=False, default=datetime.utcnow) + traceback = Column(Text(), nullable=True, default="") + """A traceback is added if a notebook fails to execute fully.""" def __repr__(self): return "{0}(pk={1})".format(self.__class__.__name__, self.pk) @@ -272,7 +135,7 @@ def to_dict(self): def format_dict( self, - cache_record: Optional[NbCacheRecord] = None, + cache_record: Optional["NbCacheRecord"] = None, path_length: Optional[int] = None, assets: bool = True, read_error: Optional[str] = None, @@ -411,3 +274,145 @@ def set_traceback(uri: str, traceback: Optional[str], db: Engine): session.commit() except IntegrityError: raise TypeError(traceback) + + +class NbCacheRecord(OrmBase): + """A record of an executed notebook cache.""" + + __tablename__ = "nbcache" + + pk = Column(Integer(), primary_key=True) + hashkey = Column(String(255), nullable=False, unique=True) + uri = Column(String(255), nullable=False, unique=False) + description = Column(String(255), nullable=False, default="") + data = Column(JSON()) + """Extra data, such as the execution time.""" + created = Column(DateTime, nullable=False, default=datetime.utcnow) + accessed = Column( + DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow + ) + + def __repr__(self): + return "{0}(pk={1})".format(self.__class__.__name__, self.pk) + + def to_dict(self): + return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} + + def format_dict( + self, hashkey=False, path_length=None, show_descript=False, show_data=True + ): + data = { + "ID": self.pk, + "Origin URI": str(shorten_path(self.uri, path_length)), + "Created": self.created.isoformat(" ", "minutes"), + "Accessed": self.accessed.isoformat(" ", "minutes"), + } + if show_descript: + data["Description"] = self.description + if hashkey: + data["Hashkey"] = self.hashkey + if show_data and self.data: + data["Data"] = self.data + return data + + @staticmethod + def create_record(uri: str, hashkey: str, db: Engine, **kwargs) -> "NbCacheRecord": + with session_context(db) as session: # type: Session + record = NbCacheRecord(hashkey=hashkey, uri=uri, **kwargs) + session.add(record) + try: + session.commit() + except IntegrityError: + raise ValueError(f"hashkey already exists:{hashkey}") + session.refresh(record) + session.expunge(record) + return record + + def remove_record(pk: int, db: Engine): + with session_context(db) as session: # type: Session + record = session.get(NbCacheRecord, pk) + session.delete(record) + session.commit() + + def remove_records(pks: List[int], db: Engine): + with session_context(db) as session: # type: Session + session.query(NbCacheRecord).filter(NbCacheRecord.pk.in_(pks)).delete( + synchronize_session=False + ) + session.commit() + + @staticmethod + def record_from_hashkey(hashkey: str, db: Engine) -> "NbCacheRecord": + with session_context(db) as session: # type: Session + result = ( + session.query(NbCacheRecord).filter_by(hashkey=hashkey).one_or_none() + ) + if result is None: + raise KeyError( + "Cache record not found for NB with hashkey: {}".format(hashkey) + ) + session.expunge(result) + return result + + @staticmethod + def record_from_pk(pk: int, db: Engine) -> "NbCacheRecord": + with session_context(db) as session: # type: Session + result = session.query(NbCacheRecord).filter_by(pk=pk).one_or_none() + if result is None: + raise KeyError("Cache record not found for NB with PK: {}".format(pk)) + session.expunge(result) + return result + + def touch(pk, db: Engine): + """Touch a record, to change its last accessed time.""" + with session_context(db) as session: # type: Session + record = session.query(NbCacheRecord).filter_by(pk=pk).one_or_none() + if record is None: + raise KeyError("Cache record not found for NB with PK: {}".format(pk)) + record.accessed = datetime.utcnow() + session.commit() + + def touch_hashkey(hashkey, db: Engine): + """Touch a record, to change its last accessed time.""" + with session_context(db) as session: # type: Session + record = ( + session.query(NbCacheRecord).filter_by(hashkey=hashkey).one_or_none() + ) + if record is None: + raise KeyError( + "Cache record not found for NB with hashkey: {}".format(hashkey) + ) + record.accessed = datetime.utcnow() + session.commit() + + @staticmethod + def records_from_uri(uri: str, db: Engine) -> "NbCacheRecord": + with session_context(db) as session: # type: Session + results = session.query(NbCacheRecord).filter_by(uri=uri).all() + session.expunge_all() + return results + + @staticmethod + def records_all(db: Engine) -> "NbCacheRecord": + with session_context(db) as session: # type: Session + results = session.query(NbCacheRecord).all() + session.expunge_all() + return results + + def records_to_delete(keep: int, db: Engine) -> List[int]: + """Return pks of the oldest records, where keep is number to keep.""" + with session_context(db) as session: # type: Session + pks_to_keep = [ + pk + for pk, in session.query(NbCacheRecord.pk) + .order_by(desc("accessed")) + .limit(keep) + .all() + ] + pks_to_delete = [ + pk + for pk, in session.query(NbCacheRecord.pk) + .filter(NbCacheRecord.pk.notin_(pks_to_keep)) + .all() + ] + return pks_to_delete From afd444473fa95b9aa2e19da01057060a8785fc25 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 13 Jan 2022 08:17:05 +0100 Subject: [PATCH 35/39] Change isort config --- jupyter_cache/base.py | 4 ++-- jupyter_cache/cache/db.py | 2 +- jupyter_cache/cache/main.py | 4 ++-- jupyter_cache/executors/base.py | 2 +- jupyter_cache/executors/basic.py | 2 +- jupyter_cache/executors/utils.py | 2 +- jupyter_cache/utils.py | 2 +- pyproject.toml | 1 + tests/test_cli.py | 2 +- 9 files changed, 11 insertions(+), 10 deletions(-) diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index f17e0d3..73071a6 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -3,14 +3,14 @@ API access to the cache should use this interface, with no assumptions about the backend storage/retrieval mechanisms. """ -import io from abc import ABC, abstractmethod +import io from pathlib import Path from typing import Iterable, List, Mapping, Optional, Tuple, Union import attr -import nbformat as nbf from attr.validators import instance_of, optional +import nbformat as nbf # TODO make these abstract from jupyter_cache.cache.db import NbCacheRecord, NbProjectRecord diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 9eecd22..293bdc7 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -1,6 +1,6 @@ -import os from contextlib import contextmanager from datetime import datetime +import os from pathlib import Path from typing import Any, Dict, List, Optional, Union diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 3d68f3c..8a5944c 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -1,9 +1,9 @@ +from contextlib import contextmanager import copy import hashlib import io -import shutil -from contextlib import contextmanager from pathlib import Path +import shutil from typing import Iterable, List, Mapping, Optional, Tuple, Union import nbformat as nbf diff --git a/jupyter_cache/executors/base.py b/jupyter_cache/executors/base.py index 260713b..b08d413 100644 --- a/jupyter_cache/executors/base.py +++ b/jupyter_cache/executors/base.py @@ -1,5 +1,5 @@ -import logging from abc import ABC, abstractmethod +import logging from typing import Any, Dict, List, Optional, Set import attr diff --git a/jupyter_cache/executors/basic.py b/jupyter_cache/executors/basic.py index c7190da..61849e8 100644 --- a/jupyter_cache/executors/basic.py +++ b/jupyter_cache/executors/basic.py @@ -1,8 +1,8 @@ import logging import multiprocessing as mproc import os -import tempfile from pathlib import Path +import tempfile from typing import NamedTuple, Tuple from jupyter_cache.base import JupyterCacheAbstract, ProjectNb diff --git a/jupyter_cache/executors/utils.py b/jupyter_cache/executors/utils.py index 3eb14f9..33f4df8 100644 --- a/jupyter_cache/executors/utils.py +++ b/jupyter_cache/executors/utils.py @@ -1,6 +1,6 @@ +from pathlib import Path import shutil import traceback -from pathlib import Path from typing import Any, List, Optional, Union import attr diff --git a/jupyter_cache/utils.py b/jupyter_cache/utils.py index 002bf2f..9f812b6 100644 --- a/jupyter_cache/utils.py +++ b/jupyter_cache/utils.py @@ -1,7 +1,7 @@ """Non-core imports in this module are lazily loaded, in order to improve CLI speed """ -import time from pathlib import Path +import time from typing import TYPE_CHECKING, List, Optional, Union from jupyter_cache.readers import NbReadError diff --git a/pyproject.toml b/pyproject.toml index dbc3d7d..b90d5e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,4 @@ build-backend = "setuptools.build_meta" [tool.isort] profile = "black" src_paths = ["jupyter_cache", "tests"] +force_sort_within_sections = true diff --git a/tests/test_cli.py b/tests/test_cli.py index ede3a53..846a70c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,8 +1,8 @@ import os from pathlib import Path -import pytest from click.testing import CliRunner +import pytest from jupyter_cache.cache.main import JupyterCacheBase from jupyter_cache.cli import CacheContext From 4c2b5b844a2ecdb3a9779fd8213108b800e50b3b Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 13 Jan 2022 08:36:02 +0100 Subject: [PATCH 36/39] Upgrade to python 3.7 --- .pre-commit-config.yaml | 6 ++++ jupyter_cache/__init__.py | 2 +- jupyter_cache/base.py | 12 ++++---- jupyter_cache/cache/db.py | 32 ++++++++-------------- jupyter_cache/cache/main.py | 16 ++++------- jupyter_cache/cli/commands/cmd_cache.py | 10 +++---- jupyter_cache/cli/commands/cmd_notebook.py | 8 +++--- jupyter_cache/executors/base.py | 6 ++-- jupyter_cache/utils.py | 6 ++-- setup.cfg | 2 +- 10 files changed, 46 insertions(+), 54 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1790269..f5763a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,12 @@ repos: args: [--no-build-isolation] additional_dependencies: [setuptools>=46.4.0] + - repo: https://github.com/asottile/pyupgrade + rev: v2.31.0 + hooks: + - id: pyupgrade + args: [--py37-plus] + - repo: https://github.com/pycqa/isort rev: 5.10.1 hooks: diff --git a/jupyter_cache/__init__.py b/jupyter_cache/__init__.py index 189d48a..9d29081 100644 --- a/jupyter_cache/__init__.py +++ b/jupyter_cache/__init__.py @@ -1,5 +1,5 @@ # NOTE: never import anything here, in order to maintain CLI speed -__version__ = "0.4.3" +__version__ = "0.5.0" def get_cache(path, cache_cls=None): diff --git a/jupyter_cache/base.py b/jupyter_cache/base.py index 73071a6..a033fbf 100644 --- a/jupyter_cache/base.py +++ b/jupyter_cache/base.py @@ -53,7 +53,7 @@ class ProjectNb: ) nb: nbf.NotebookNode = attr.ib( validator=instance_of(nbf.NotebookNode), - repr=lambda nb: "Notebook(cells={0})".format(len(nb.cells)), + repr=lambda nb: f"Notebook(cells={len(nb.cells)})", metadata={"help": "the notebook"}, ) assets: List[Path] = attr.ib( @@ -75,9 +75,7 @@ def __iter__(self) -> Iterable[Tuple[Path, io.BufferedReader]]: """Yield the relative path and open files (in bytes mode)""" def __repr__(self): - return "{0}(paths={1})".format( - self.__class__.__name__, len(self.relative_paths) - ) + return f"{self.__class__.__name__}(paths={len(self.relative_paths)})" @attr.s(frozen=True, slots=True) @@ -86,7 +84,7 @@ class CacheBundleIn: nb: nbf.NotebookNode = attr.ib( validator=instance_of(nbf.NotebookNode), - repr=lambda nb: "Notebook(cells={0})".format(len(nb.cells)), + repr=lambda nb: f"Notebook(cells={len(nb.cells)})", metadata={"help": "the notebook"}, ) uri: str = attr.ib( @@ -119,7 +117,7 @@ class CacheBundleOut: nb: nbf.NotebookNode = attr.ib( validator=instance_of(nbf.NotebookNode), - repr=lambda nb: "Notebook(cells={0})".format(len(nb.cells)), + repr=lambda nb: f"Notebook(cells={len(nb.cells)})", metadata={"help": "the notebook"}, ) record: NbCacheRecord = attr.ib(metadata={"help": "the cache record"}) @@ -282,7 +280,7 @@ def add_nb_to_project( uri: str, *, read_data: Mapping = DEFAULT_READ_DATA, - assets: List[str] = () + assets: List[str] = (), ) -> NbProjectRecord: """Add a single notebook to the project. diff --git a/jupyter_cache/cache/db.py b/jupyter_cache/cache/db.py index 293bdc7..3217a19 100644 --- a/jupyter_cache/cache/db.py +++ b/jupyter_cache/cache/db.py @@ -30,7 +30,7 @@ def create_db(path: Union[str, Path]) -> Engine: :param path: The path to the cache folder. """ exists = (Path(path) / DB_NAME).exists() - engine = create_engine("sqlite:///{}".format(os.path.join(path, DB_NAME))) + engine = create_engine(f"sqlite:///{os.path.join(path, DB_NAME)}") if not exists: # add all the tables, and a version identifier OrmBase.metadata.create_all(engine) @@ -74,7 +74,7 @@ class Setting(OrmBase): value = Column(JSON()) def __repr__(self): - return "{0}(pk={1},{2}={3})".format( + return "{}(pk={},{}={})".format( self.__class__.__name__, self.pk, self.key, self.value ) @@ -99,7 +99,7 @@ def get_value(key: str, db: Engine, default=None): if default is not None: result = [default] else: - raise KeyError("Setting not found in DB: {}".format(key)) + raise KeyError(f"Setting not found in DB: {key}") value = result[0] return value @@ -128,7 +128,7 @@ class NbProjectRecord(OrmBase): """A traceback is added if a notebook fails to execute fully.""" def __repr__(self): - return "{0}(pk={1})".format(self.__class__.__name__, self.pk) + return f"{self.__class__.__name__}(pk={self.pk})" def to_dict(self): return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} @@ -232,7 +232,7 @@ def record_from_pk(pk: int, db: Engine) -> "NbProjectRecord": with session_context(db) as session: # type: Session result = session.query(NbProjectRecord).filter_by(pk=pk).one_or_none() if result is None: - raise KeyError("Project record not found for NB with PK: {}".format(pk)) + raise KeyError(f"Project record not found for NB with PK: {pk}") session.expunge(result) return result @@ -241,9 +241,7 @@ def record_from_uri(uri: str, db: Engine) -> "NbProjectRecord": with session_context(db) as session: # type: Session result = session.query(NbProjectRecord).filter_by(uri=uri).one_or_none() if result is None: - raise KeyError( - "Project record not found for NB with URI: {}".format(uri) - ) + raise KeyError(f"Project record not found for NB with URI: {uri}") session.expunge(result) return result @@ -266,9 +264,7 @@ def set_traceback(uri: str, traceback: Optional[str], db: Engine): with session_context(db) as session: # type: Session result = session.query(NbProjectRecord).filter_by(uri=uri).one_or_none() if result is None: - raise KeyError( - "Project record not found for NB with URI: {}".format(uri) - ) + raise KeyError(f"Project record not found for NB with URI: {uri}") result.traceback = traceback try: session.commit() @@ -293,7 +289,7 @@ class NbCacheRecord(OrmBase): ) def __repr__(self): - return "{0}(pk={1})".format(self.__class__.__name__, self.pk) + return f"{self.__class__.__name__}(pk={self.pk})" def to_dict(self): return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} @@ -348,9 +344,7 @@ def record_from_hashkey(hashkey: str, db: Engine) -> "NbCacheRecord": session.query(NbCacheRecord).filter_by(hashkey=hashkey).one_or_none() ) if result is None: - raise KeyError( - "Cache record not found for NB with hashkey: {}".format(hashkey) - ) + raise KeyError(f"Cache record not found for NB with hashkey: {hashkey}") session.expunge(result) return result @@ -359,7 +353,7 @@ def record_from_pk(pk: int, db: Engine) -> "NbCacheRecord": with session_context(db) as session: # type: Session result = session.query(NbCacheRecord).filter_by(pk=pk).one_or_none() if result is None: - raise KeyError("Cache record not found for NB with PK: {}".format(pk)) + raise KeyError(f"Cache record not found for NB with PK: {pk}") session.expunge(result) return result @@ -368,7 +362,7 @@ def touch(pk, db: Engine): with session_context(db) as session: # type: Session record = session.query(NbCacheRecord).filter_by(pk=pk).one_or_none() if record is None: - raise KeyError("Cache record not found for NB with PK: {}".format(pk)) + raise KeyError(f"Cache record not found for NB with PK: {pk}") record.accessed = datetime.utcnow() session.commit() @@ -379,9 +373,7 @@ def touch_hashkey(hashkey, db: Engine): session.query(NbCacheRecord).filter_by(hashkey=hashkey).one_or_none() ) if record is None: - raise KeyError( - "Cache record not found for NB with hashkey: {}".format(hashkey) - ) + raise KeyError(f"Cache record not found for NB with hashkey: {hashkey}") record.accessed = datetime.utcnow() session.commit() diff --git a/jupyter_cache/cache/main.py b/jupyter_cache/cache/main.py index 8a5944c..ea63dbe 100644 --- a/jupyter_cache/cache/main.py +++ b/jupyter_cache/cache/main.py @@ -74,7 +74,7 @@ def db(self): return self._db def __repr__(self): - return "{0}({1})".format(self.__class__.__name__, repr(str(self._path))) + return f"{self.__class__.__name__}({repr(str(self._path))})" def __getstate__(self): """For pickling instances, db must be removed.""" @@ -94,7 +94,7 @@ def _get_notebook_path_cache(self, hashkey, raise_on_missing=False) -> Path: """Retrieve a relative path in the cache to a notebook, from its hash.""" path = self.path.joinpath(Path("executed", hashkey, "base.ipynb")) if not path.exists() and raise_on_missing: - raise RetrievalError("hashkey not in cache: {}".format(hashkey)) + raise RetrievalError(f"hashkey not in cache: {hashkey}") return path def _get_artifact_path_cache(self, hashkey) -> Path: @@ -297,9 +297,7 @@ def get_cache_bundle(self, pk: int) -> CacheBundleOut: path = self._get_notebook_path_cache(record.hashkey) artifact_folder = self._get_artifact_path_cache(record.hashkey) if not path.exists(): - raise KeyError( - "Notebook file does not exist for cache record PK: {}".format(pk) - ) + raise KeyError(f"Notebook file does not exist for cache record PK: {pk}") return CacheBundleOut( nbf.reads(path.read_text(encoding="utf8"), nbf.NO_CONVERT), @@ -327,9 +325,7 @@ def remove_cache(self, pk: int): record = NbCacheRecord.record_from_pk(pk, self.db) path = self._get_notebook_path_cache(record.hashkey) if not path.exists(): - raise KeyError( - "Notebook file does not exist for cache record PK: {}".format(pk) - ) + raise KeyError(f"Notebook file does not exist for cache record PK: {pk}") shutil.rmtree(path.parent) NbCacheRecord.remove_records([pk], self.db) @@ -466,8 +462,8 @@ def get_project_notebook(self, uri_or_pk: Union[int, str]) -> ProjectNb: else: record = NbProjectRecord.record_from_uri(uri_or_pk, self.db) if not Path(record.uri).exists(): - raise IOError( - "The URI of the project record no longer exists: {}".format(record.uri) + raise OSError( + f"The URI of the project record no longer exists: {record.uri}" ) try: reader = get_reader(record.read_data) diff --git a/jupyter_cache/cli/commands/cmd_cache.py b/jupyter_cache/cli/commands/cmd_cache.py index 5427caf..5194ef1 100644 --- a/jupyter_cache/cli/commands/cmd_cache.py +++ b/jupyter_cache/cli/commands/cmd_cache.py @@ -55,7 +55,7 @@ def cached_info(cache, pk): try: record = db.get_cache_record(pk) except KeyError: - click.secho("ID {} does not exist, Aborting!".format(pk), fg="red") + click.secho(f"ID {pk} does not exist, Aborting!", fg="red") raise click.Abort() data = record.format_dict(hashkey=True, path_length=None) click.echo(yaml.safe_dump(data, sort_keys=False), nl=False) @@ -93,7 +93,7 @@ def cache_file(db, nbpath, validate, overwrite, artifact_paths=()): from jupyter_cache.base import NbValidityError - click.echo("Caching: {}".format(nbpath)) + click.echo(f"Caching: {nbpath}") try: db.cache_notebook_file( nbpath, @@ -112,11 +112,11 @@ def cache_file(db, nbpath, validate, overwrite, artifact_paths=()): check_validity=False, overwrite=overwrite, ) - except IOError as error: + except OSError as error: click.secho("Artifact Error: ", fg="red", nl=False) click.echo(str(error)) return False - except IOError as error: + except OSError as error: click.secho("Artifact Error: ", fg="red", nl=False) click.echo(str(error)) return False @@ -182,7 +182,7 @@ def remove_caches(cache, pks, remove_all): pks = [r.pk for r in db.list_cache_records()] for pk in pks: # TODO deal with errors (print all at end? or option to ignore) - click.echo("Removing Cache ID = {}".format(pk)) + click.echo(f"Removing Cache ID = {pk}") try: db.remove_cache(pk) except KeyError: diff --git a/jupyter_cache/cli/commands/cmd_notebook.py b/jupyter_cache/cli/commands/cmd_notebook.py index 13c6d4a..5f41fb2 100644 --- a/jupyter_cache/cli/commands/cmd_notebook.py +++ b/jupyter_cache/cli/commands/cmd_notebook.py @@ -30,7 +30,7 @@ def add_notebooks(cache, nbpaths, reader): db = cache.get_cache() for path in nbpaths: # TODO deal with errors (print all at end? or option to ignore) - click.echo("Adding: {}".format(path)) + click.echo(f"Adding: {path}") db.add_nb_to_project(path, read_data={"name": reader, "type": "plugin"}) click.secho("Success!", fg="green") @@ -72,7 +72,7 @@ def remove_nbs(cache, pk_paths): db = cache.get_cache() for pk_path in pk_paths: # TODO deal with errors (print all at end? or option to ignore) - click.echo("Removing: {}".format(pk_path)) + click.echo(f"Removing: {pk_path}") db.remove_nb_from_project( int(pk_path) if pk_path.isdigit() else os.path.abspath(pk_path) ) @@ -90,7 +90,7 @@ def invalidate_nbs(cache, pk_paths, invalidate_all): pk_paths = [str(record.pk) for record in db.list_project_records()] for pk_path in pk_paths: # TODO deal with errors (print all at end? or option to ignore) - click.echo("Invalidating: {}".format(pk_path)) + click.echo(f"Invalidating: {pk_path}") record = db.get_cached_project_nb( int(pk_path) if pk_path.isdigit() else os.path.abspath(pk_path) ) @@ -145,7 +145,7 @@ def show_project_record(cache, pk_path, tb): int(pk_path) if pk_path.isdigit() else os.path.abspath(pk_path) ) except KeyError: - click.secho("ID {} does not exist, Aborting!".format(pk_path), fg="red") + click.secho(f"ID {pk_path} does not exist, Aborting!", fg="red") raise click.Abort() cache_record = None try: diff --git a/jupyter_cache/executors/base.py b/jupyter_cache/executors/base.py index b08d413..26e35cd 100644 --- a/jupyter_cache/executors/base.py +++ b/jupyter_cache/executors/base.py @@ -51,7 +51,7 @@ def __init__(self, cache: JupyterCacheAbstract, logger=None): self._logger = logger or logging.getLogger(__name__) def __repr__(self): - return "{0}(cache={1})".format(self.__class__.__name__, self._cache) + return f"{self.__class__.__name__}(cache={self._cache})" @property def cache(self): @@ -91,7 +91,7 @@ def run_and_cache( timeout: Optional[int] = 30, allow_errors: bool = False, force: bool = False, - **kwargs: Any + **kwargs: Any, ) -> ExecutorRunResult: """Run execution, cache successfully executed notebooks and return their URIs @@ -116,7 +116,7 @@ def load_executor( ep = get_entry_point(ENTRY_POINT_GROUP_EXEC, entry_point) if ep is None: raise ImportError( - "Entry point not found: {}:{}".format(ENTRY_POINT_GROUP_EXEC, entry_point) + f"Entry point not found: {ENTRY_POINT_GROUP_EXEC}:{entry_point}" ) execute_cls = ep.load() return execute_cls(cache=cache, logger=logger) diff --git a/jupyter_cache/utils.py b/jupyter_cache/utils.py index 9f812b6..db40a97 100644 --- a/jupyter_cache/utils.py +++ b/jupyter_cache/utils.py @@ -29,13 +29,13 @@ def to_relative_paths( for path in paths: path = Path(path).absolute() if check_existence and not path.exists(): - raise IOError(f"Path does not exist: {path}") + raise OSError(f"Path does not exist: {path}") if check_existence and not path.is_file(): - raise IOError(f"Path is not a file: {path}") + raise OSError(f"Path is not a file: {path}") try: rel_path = path.relative_to(folder) except ValueError: - raise IOError(f"Path '{path}' is not in folder '{folder}''") + raise OSError(f"Path '{path}' is not in folder '{folder}''") rel_paths.append(rel_path) return rel_paths diff --git a/setup.cfg b/setup.cfg index a1e56f3..3d55bb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ install_requires = pyyaml sqlalchemy>=1.3.12,<1.5 tabulate -python_requires = ~=3.6 +python_requires = ~=3.7 include_package_data = True zip_safe = True From c473dc27247fd490a11eef68d8fefc4c9f5b606a Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 13 Jan 2022 08:39:21 +0100 Subject: [PATCH 37/39] reduce version, so that docs build --- jupyter_cache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_cache/__init__.py b/jupyter_cache/__init__.py index 9d29081..189d48a 100644 --- a/jupyter_cache/__init__.py +++ b/jupyter_cache/__init__.py @@ -1,5 +1,5 @@ # NOTE: never import anything here, in order to maintain CLI speed -__version__ = "0.5.0" +__version__ = "0.4.3" def get_cache(path, cache_cls=None): From 979c051d98d8fb4f88c3a898dcbcc5b2222858e4 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 13 Jan 2022 08:48:05 +0100 Subject: [PATCH 38/39] add emojis to docs --- docs/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index ab5d54b..98c1b5e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,16 +2,16 @@ Execute and cache multiple Jupyter Notebook-like files via an [API](use/api) and [CLI](use/cli). -Smart re-execution +🤓 Smart re-execution : Notebooks will only be re-executed when **code cells** have changed (or code related metadata), not Markdown/Raw cells. -Pluggable execution modes +🧩 Pluggable execution modes : Select the executor for notebooks, including serial and parallel execution -Execution reports +📈 Execution reports : Timing statistics and exception tracebacks are stored for analysis -[jupytext](https://jupytext.readthedocs.io) integration +📖 [jupytext](https://jupytext.readthedocs.io) integration : Read and execute notebooks written in multiple formats ## Installation From 1d3fd4bb00bff34273edd914b832e01680d444c0 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Fri, 14 Jan 2022 07:23:17 +0100 Subject: [PATCH 39/39] Add "Why use jupyter-cache?" docs section --- README.md | 32 +++++++++++++++++++++++--------- docs/index.md | 12 ++++++++++++ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0115b98..84e8b11 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,17 @@ A defined interface for working with a cache of jupyter notebooks. -Some desired requirements (not yet all implemented): +## Why use jupyter-cache? -- Persistent -- Separates out "edits to content" from "edits to code cells". Cell - rearranges and code cell changes should require a re-execution. Content changes should not. -- Allow parallel access to notebooks (for execution) -- Store execution statistics/reports -- Store external assets: Notebooks being executed often require external assets: importing scripts/data/etc. These are prepared by the users. -- Store execution artefacts: created during execution -- A transparent and robust cache invalidation: imagine the user updating an external dependency or a Python module, or checking out a different git branch. +If you have a number of notebooks whose execution outputs you want to ensure are kept up to date, without having to re-execute them every time (particularly for long running code, or text-based formats that do not store the outputs). + +The notebooks must have deterministic execution outputs: + +- You use the same environment to run them (e.g. the same installed packages) +- They run no non-deterministic code (e.g. random numbers) +- They do not depend on external resources (e.g. files or network connections) that change over time + +For example, it is utilised by [jupyter-book](https://jupyterbook.org/content/execute.html#caching-the-notebook-execution), to allow for fast document re-builds. ## Install @@ -36,6 +37,19 @@ pip install -e .[cli,code_style,testing] See the documentation for usage. +## Development + +Some desired requirements (not yet all implemented): + +- Persistent +- Separates out "edits to content" from "edits to code cells". Cell + rearranges and code cell changes should require a re-execution. Content changes should not. +- Allow parallel access to notebooks (for execution) +- Store execution statistics/reports +- Store external assets: Notebooks being executed often require external assets: importing scripts/data/etc. These are prepared by the users. +- Store execution artefacts: created during execution +- A transparent and robust cache invalidation: imagine the user updating an external dependency or a Python module, or checking out a different git branch. + ## Contributing jupyter-cache follows the [Executable Book Contribution Guide](https://executablebooks.org/en/latest/contributing.html). We'd love your help! diff --git a/docs/index.md b/docs/index.md index 98c1b5e..32a483e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,6 +14,18 @@ Execute and cache multiple Jupyter Notebook-like files via an [API](use/api) and 📖 [jupytext](https://jupytext.readthedocs.io) integration : Read and execute notebooks written in multiple formats +## Why use jupyter-cache? + +If you have a number of notebooks whose execution outputs you want to ensure are kept up to date, without having to re-execute them every time (particularly for long running code, or text-based formats that do not store the outputs). + +The notebooks must have deterministic execution outputs: + +- You use the same environment to run them (e.g. the same installed packages) +- They run no non-deterministic code (e.g. random numbers) +- They do not depend on external resources (e.g. files or network connections) that change over time + +For example, it is utilised by [jupyter-book](https://jupyterbook.org/content/execute.html#caching-the-notebook-execution), to allow for fast document re-builds. + ## Installation Install `jupyter-cache`, via pip or Conda: