├── .github └── workflows │ ├── build.yml │ ├── latest-changes.jinja2 │ └── latest-changes.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CITATION.cff ├── LICENSE ├── README.md ├── docs ├── changelog.md ├── faq.md ├── faq │ ├── cell-source-string.ipynb │ ├── example-after-init-set-filename.ipynb │ ├── example-project-uninitialized.md │ ├── example-project-uninitialized │ │ └── 2022-07-13-my-task-x.ipynb │ ├── example-project.md │ ├── example-project │ │ ├── 2022-05-13-my-task-x.ipynb │ │ ├── 2022-07-18-my-task-y.ipynb │ │ └── nbproject_metadata.yml │ ├── header-author-field.ipynb │ ├── inconsistent-packages-parents-no-store.ipynb │ ├── inconsistent-packages-parents-store.ipynb │ ├── initialize-set-env.ipynb │ ├── initialize.ipynb │ ├── internal-functions.ipynb │ ├── not-initialized.ipynb │ ├── publish-not-last-cell.ipynb │ ├── publish-set-version.ipynb │ ├── publish-without-saving.ipynb │ ├── publish-without-title.ipynb │ ├── publish-wrapper.ipynb │ ├── set-env-via-environment-var.ipynb │ ├── setup.md │ ├── title-not-at-top.ipynb │ └── trigger-exit-upon-init.ipynb ├── guide.md ├── guide │ ├── basic-metadata.ipynb │ ├── meta.ipynb │ ├── received.ipynb │ └── update-metadata.ipynb ├── index.md ├── quickstart.ipynb └── reference.md ├── lamin-project.yaml ├── nbproject ├── __init__.py ├── __main__.py ├── _cli.py ├── _header.py ├── _is_run_from_ipython.py ├── _logger.py ├── _meta.py ├── _publish.py ├── _schemas.py └── dev │ ├── __init__.py │ ├── _check_last_cell.py │ ├── _classic_nb_commands.py │ ├── _consecutiveness.py │ ├── _frontend_commands.py │ ├── _initialize.py │ ├── _jupyter_communicate.py │ ├── _jupyter_lab_commands.py │ ├── _lamin_communicate.py │ ├── _meta_live.py │ ├── _meta_store.py │ ├── _metadata_display.py │ ├── _notebook.py │ ├── _pypackage.py │ └── _set_version.py ├── noxfile.py ├── pyproject.toml └── tests ├── conftest.py ├── for-nbconvert.ipynb ├── test_cli.py ├── test_jupyter.py ├── test_nbconvert.py ├── test_notebooks.py └── test_set_version.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: ["3.10", "3.13"] 16 | 17 | steps: 18 | - name: Checkout main 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - name: Checkout lndocs 23 | uses: actions/checkout@v3 24 | with: 25 | repository: laminlabs/lndocs 26 | ssh-key: ${{ secrets.READ_LNDOCS }} 27 | path: lndocs 28 | ref: main 29 | 30 | - name: Setup Python 31 | uses: actions/setup-python@v4 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | cache: "pip" 35 | cache-dependency-path: ".github/workflows/build.yml" # See dependencies below 36 | 37 | - name: Cache nox 38 | uses: actions/cache@v3 39 | with: 40 | path: .nox 41 | key: nox-${{ runner.os }} 42 | 43 | - name: Cache pre-commit 44 | uses: actions/cache@v3 45 | with: 46 | path: ~/.cache/pre-commit 47 | key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} 48 | 49 | - name: Install Python dependencies 50 | run: | 51 | python -m pip install -U pip 52 | pip install -U laminci 53 | 54 | - name: Configure AWS 55 | uses: aws-actions/configure-aws-credentials@v2 56 | with: 57 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 58 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 59 | aws-region: eu-central-1 60 | # - run: nox -s lint 61 | - run: nox -s build 62 | 63 | - name: Codecov 64 | if: matrix.python-version == '3.13' 65 | uses: codecov/codecov-action@v2 66 | with: 67 | token: ${{ secrets.CODECOV_TOKEN }} 68 | 69 | - uses: cloudflare/pages-action@v1 70 | id: cloudflare 71 | with: 72 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 73 | accountId: 472bdad691b4483dea759eadb37110bd 74 | projectName: nbproject 75 | directory: "_build/html" 76 | gitHubToken: ${{ secrets.GITHUB_TOKEN }} 77 | wranglerVersion: "3" 78 | -------------------------------------------------------------------------------- /.github/workflows/latest-changes.jinja2: -------------------------------------------------------------------------------- 1 | {{pr.title}} | [{{pr.number}}]({{pr.html_url}}) | [{{pr.user.login}}]({{pr.user.html_url}}) | {{pr.closed_at.date().isoformat()}} | 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/latest-changes.yml: -------------------------------------------------------------------------------- 1 | name: latest-changes 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - main 7 | types: 8 | - closed 9 | workflow_dispatch: 10 | inputs: 11 | number: 12 | description: PR number 13 | required: true 14 | 15 | jobs: 16 | latest-changes: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: docker://tiangolo/latest-changes:0.0.3 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | latest_changes_file: docs/changelog.md 24 | latest_changes_header: '--- \| --- \| --- \| --- \| ---\n' 25 | template_file: ./.github/workflows/latest-changes.jinja2 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # Lamin 134 | _build 135 | docs/nbproject.* 136 | lamin_sphinx 137 | docs/conf.py 138 | _docs_tmp* 139 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: false 2 | default_language_version: 3 | python: python3 4 | default_stages: 5 | - commit 6 | - push 7 | minimum_pre_commit_version: 2.16.0 8 | repos: 9 | - repo: https://github.com/pre-commit/mirrors-prettier 10 | rev: v4.0.0-alpha.4 11 | hooks: 12 | - id: prettier 13 | exclude: | 14 | (?x)( 15 | docs/changelog.md 16 | ) 17 | - repo: https://github.com/kynan/nbstripout 18 | rev: 0.6.1 19 | hooks: 20 | - id: nbstripout 21 | exclude: | 22 | (?x)( 23 | docs/examples/| 24 | docs/notes/| 25 | cell-source-string.ipynb 26 | ) 27 | - repo: https://github.com/astral-sh/ruff-pre-commit 28 | rev: v0.1.7 29 | hooks: 30 | - id: ruff 31 | args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes] 32 | - id: ruff-format 33 | - repo: https://github.com/pre-commit/pre-commit-hooks 34 | rev: v4.5.0 35 | hooks: 36 | - id: detect-private-key 37 | - id: check-ast 38 | - id: end-of-file-fixer 39 | exclude: | 40 | (?x)( 41 | .github/workflows/latest-changes.jinja2 42 | ) 43 | - id: mixed-line-ending 44 | args: [--fix=lf] 45 | - id: trailing-whitespace 46 | - id: check-case-conflict 47 | - repo: https://github.com/pre-commit/mirrors-mypy 48 | rev: v1.7.1 49 | hooks: 50 | - id: mypy 51 | args: [--no-strict-optional, --ignore-missing-imports] 52 | additional_dependencies: ["types-requests", "types-attrs"] 53 | exclude: | 54 | (?x)( 55 | test_notebooks.py 56 | ) 57 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: "nbproject: Manage Jupyter notebooks" 3 | authors: 4 | - family-names: Rybakov 5 | given-names: Sergei 6 | orcid: https://orcid.org/0000-0002-4944-6586 7 | - family-names: Wolf 8 | given-names: Alex 9 | orcid: https://orcid.org/0000-0002-8760-7838 10 | url: https://github.com/laminlabs/nbproject 11 | preferred-citation: 12 | type: article 13 | title: "nbproject: Manage Jupyter notebooks" 14 | authors: 15 | - family-names: Rybakov 16 | given-names: Sergei 17 | orcid: https://orcid.org/0000-0002-4944-6586 18 | - family-names: Heumos 19 | given-names: Lukas 20 | orcid: https://orcid.org/0000-0002-8937-3457 21 | - family-names: Wolf 22 | given-names: Alex 23 | orcid: https://orcid.org/0000-0002-8760-7838 24 | doi: 10.56528/nbp 25 | journal: Lamin Reports 26 | year: 2022 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stars](https://img.shields.io/github/stars/laminlabs/nbproject?logo=GitHub&color=yellow)](https://github.com/laminlabs/nbproject) 2 | [![coverage](https://codecov.io/gh/laminlabs/nbproject/branch/main/graph/badge.svg?token=05R04PR9RB)](https://codecov.io/gh/laminlabs/nbproject) 3 | [![pypi](https://img.shields.io/pypi/v/nbproject?color=blue&label=pypi%20package)](https://pypi.org/project/nbproject) 4 | [![doi](https://img.shields.io/badge/doi-10.56528%2Fnbp-lightgrey)](https://doi.org/10.56528/nbp) 5 | 6 | # nbproject 7 | 8 | Light-weight Jupyter notebook manager. Track metadata, imports, and integrity. 9 | 10 | --- 11 | 12 | 💡 We recommend [lamindb.track()](https://lamin.ai/docs/lamindb.track) instead of `nbproject` to: 13 | 14 | - consistently track data sources across notebooks, pipelines & apps 15 | - full provenance for datasets that you pull and push from notebooks 16 | - manage notebook copying & integrate with Google Colab 17 | - broader compatibility 18 | 19 | Like `nbproject`, `lamindb` is open-source. 20 | 21 | `nbproject` will continue to be maintained as a utility for `lamindb`. 22 | 23 | --- 24 | 25 | Install: ![pyversions](https://img.shields.io/pypi/pyversions/nbproject) 26 | 27 | ``` 28 | pip install nbproject 29 | ``` 30 | 31 | Also consider installing `ipylab` for interactive features 32 | if you use `nbpoject` on `jupyter lab` 33 | 34 | ``` 35 | pip install ipylab 36 | ``` 37 | 38 | Get started: 39 | 40 | ``` 41 | import nbproject 42 | 43 | nbproject.header() # Tracks notebook, displays metadata 44 | 45 | # do things 46 | 47 | nbproject.publish() # Checks consecutiveness & title, sets version & imported python packages 48 | ``` 49 | 50 | More: Read the [docs](https://nbproject.lamin.ai). 51 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | Name | PR | Developer | Date | Version 5 | --- | --- | --- | --- | --- 6 | 🚸 Error for nbconvert if `--inplace` is not passed | [289](https://github.com/laminlabs/nbproject/pull/289) | [falexwolf](https://github.com/falexwolf) | 2025-04-30 | 7 | 🚸 Support inferring notebook path when executed through nbconvert | [288](https://github.com/laminlabs/nbproject/pull/288) | [falexwolf](https://github.com/falexwolf) | 2025-04-18 | 8 | 🐛 Fix `__vsc_ipynb_file__` unaccessible through `get_ipython().user_ns` in VS Code | [287](https://github.com/laminlabs/nbproject/pull/287) | [Koncopd](https://github.com/Koncopd) | 2025-03-08 | 9 | 🐛 Account for cell source being a string | [286](https://github.com/laminlabs/nbproject/pull/286) | [Koncopd](https://github.com/Koncopd) | 2024-10-16 | 10 | ⬆️ Upgrade to pydantic v2 | [284](https://github.com/laminlabs/nbproject/pull/284) | [falexwolf](https://github.com/falexwolf) | 2024-07-23 | 11 | ⚡️ Warn instead of raising the exception when ipylab is not installed | [283](https://github.com/laminlabs/nbproject/pull/283) | [Koncopd](https://github.com/Koncopd) | 2024-05-08 | 0.10.3 12 | ♻️ Make ipylab an optional dependency | [282](https://github.com/laminlabs/nbproject/pull/282) | [falexwolf](https://github.com/falexwolf) | 2024-05-06 | 13 | 🔇 Silence erroneous logging | [279](https://github.com/laminlabs/nbproject/pull/279) | [falexwolf](https://github.com/falexwolf) | 2024-02-27 | 0.10.1 14 | 🚸 Init version at 1 | [277](https://github.com/laminlabs/nbproject/pull/277) | [falexwolf](https://github.com/falexwolf) | 2024-01-11 | 0.10.0 15 | 🚸 Improve error message when empty notebook is passed | [275](https://github.com/laminlabs/nbproject/pull/275) | [Zethson](https://github.com/Zethson) | 2024-01-04 | 16 | 👷 Ruff and 3.11 | [276](https://github.com/laminlabs/nbproject/pull/276) | [Zethson](https://github.com/Zethson) | 2023-12-26 | 17 | 🩹 Account for empty notebook in consecutiveness check | [272](https://github.com/laminlabs/nbproject/pull/272) | [falexwolf](https://github.com/falexwolf) | 2023-10-01 | 18 | 📝 Fix image link | [271](https://github.com/laminlabs/nbproject/pull/271) | [falexwolf](https://github.com/falexwolf) | 2023-09-18 | 19 | 🚸 More robust title parsing | [270](https://github.com/laminlabs/nbproject/pull/270) | [falexwolf](https://github.com/falexwolf) | 2023-08-15 | 0.9.2 20 | 📝 Simplify docs, adopt new lamin.ai static site architecture | [269](https://github.com/laminlabs/nbproject/pull/269) | [falexwolf](https://github.com/falexwolf) | 2023-08-04 | 0.9.1 21 | ♻️ Remove loguru-dependent call, expand `metadata_only` return signature | [268](https://github.com/laminlabs/nbproject/pull/268) | [falexwolf](https://github.com/falexwolf) | 2023-06-15 | 0.9.0 22 | 📝 Position nbproject | [264](https://github.com/laminlabs/nbproject/pull/264) | [falexwolf](https://github.com/falexwolf) | 2023-06-15 | 23 | 🩹 Try to get vs code path first | [267](https://github.com/laminlabs/nbproject/pull/267) | [Koncopd](https://github.com/Koncopd) | 2023-06-10 | 24 | 🦺 Safer parsing during initialization | [266](https://github.com/laminlabs/nbproject/pull/266) | [Koncopd](https://github.com/Koncopd) | 2023-06-03 | 0.8.7 25 | ➖ Remove nbproject-test from dependencies | [262](https://github.com/laminlabs/nbproject/pull/262) | [Koncopd](https://github.com/Koncopd) | 2023-05-30 | 26 | 🐛 Safer ipylab initialization | [263](https://github.com/laminlabs/nbproject/pull/263) | [Koncopd](https://github.com/Koncopd) | 2023-04-18 | 0.8.6 27 | 🐛 Fix VS Code integration for recent VS Code versions | [260](https://github.com/laminlabs/nbproject/pull/260) | [Koncopd](https://github.com/Koncopd) | 2023-04-17 | 0.8.5 28 | ♻️ Replace `display` with `metadata_only` arg in `header` | [257](https://github.com/laminlabs/nbproject/pull/257) | [Koncopd](https://github.com/Koncopd) | 2023-04-10 | 0.8.4 29 | 🚸 Allow to not display metadata | [255](https://github.com/laminlabs/nbproject/pull/255) | [falexwolf](https://github.com/falexwolf) | 2023-03-12 | 0.8.3 30 | ⬆️ Observe `lndb-setup` rename to `lndb` | [253](https://github.com/laminlabs/nbproject/pull/253) | [bpenteado](https://github.com/bpenteado) | 2023-02-16 | 0.8.2 31 | ✨ Safer notebook path inference and more ways to do it | [251](https://github.com/laminlabs/nbproject/pull/251) | [Koncopd](https://github.com/Koncopd) | 2023-02-10 | 32 | 🐛 Check keys before accessing in `notebook_path` | [250](https://github.com/laminlabs/nbproject/pull/250) | [Koncopd](https://github.com/Koncopd) | 2023-02-08 | 33 | :zap: Do nothing with ipylab when not in ipython | [247](https://github.com/laminlabs/nbproject/pull/247) | [Koncopd](https://github.com/Koncopd) | 2022-11-14 | 34 | 🚸 Only init `JupyterFrontEnd` if run within ipython | [246](https://github.com/laminlabs/nbproject/pull/246) | [falexwolf](https://github.com/falexwolf) | 2022-11-13 | 0.8.1 35 | 🚸 Do no longer auto-bump `version` upon `publish()` Breaking | [244](https://github.com/laminlabs/nbproject/pull/244) | [falexwolf](https://github.com/falexwolf) | 2022-11-12 | 0.8.0 36 | 🚚 Rename `version` 'draft' to '0' Breaking | [243](https://github.com/laminlabs/nbproject/pull/243) | [falexwolf](https://github.com/falexwolf) | 2022-11-12 | 37 | 💄 Fix PyPI badge color & change badge order | [242](https://github.com/laminlabs/nbproject/pull/242) | [falexwolf](https://github.com/falexwolf) | 2022-10-24 | 38 | ♻️ Read user name from settings | [239](https://github.com/laminlabs/nbproject/pull/239) | [falexwolf](https://github.com/falexwolf) | 2022-10-10 | 0.7.0 39 | ✨ Add `author` field and a few fixes | [238](https://github.com/laminlabs/nbproject/pull/238) | [Koncopd](https://github.com/Koncopd) | 2022-10-10 | 0.5.5 40 | ♻️ Refactor publish | [237](https://github.com/laminlabs/nbproject/pull/237) | [falexwolf](https://github.com/falexwolf) | 2022-10-07 | 0.5.4 41 | ✨ Allow setting nbproject `env` via environment variable | [236](https://github.com/laminlabs/nbproject/pull/236) | [Koncopd](https://github.com/Koncopd) | 2022-09-30 | 42 | 📝 Expand some docstrings | [235](https://github.com/laminlabs/nbproject/pull/235) | [Koncopd](https://github.com/Koncopd) | 2022-09-30 | 43 | 💄 Strip newline from title | [234](https://github.com/laminlabs/nbproject/pull/234) | [falexwolf](https://github.com/falexwolf) | 2022-09-24 | 0.5.3 44 | 🚚 Rename tutorials to guide, guides to FAQ | [230](https://github.com/laminlabs/nbproject/pull/230) | [falexwolf](https://github.com/falexwolf) | 2022-09-05 | 45 | 📝 Add GitHub stars to docs | [229](https://github.com/laminlabs/nbproject/pull/229) | [falexwolf](https://github.com/falexwolf) | 2022-08-30 | 0.5.2 46 | 🩹 Identify VS Code as an environment | [228](https://github.com/laminlabs/nbproject/pull/228) | [Koncopd](https://github.com/Koncopd) | 2022-08-30 | 47 | 🎨 Strip quickstart output, upgrade `nbproject_test` | [227](https://github.com/laminlabs/nbproject/pull/227) | [falexwolf](https://github.com/falexwolf) | 2022-08-29 | 48 | 📝 Make a setup guide | [226](https://github.com/laminlabs/nbproject/pull/226) | [falexwolf](https://github.com/falexwolf) | 2022-08-29 | 49 | 📝 Installation instructions and other things | [225](https://github.com/laminlabs/nbproject/pull/225) | [Koncopd](https://github.com/Koncopd) | 2022-08-28 | 0.5.1 50 | 🚸 Additional warnings and safety features | [220](https://github.com/laminlabs/nbproject/pull/220) | [Koncopd](https://github.com/Koncopd) | 2022-08-25 | 51 | ✨ Add interactivity for classic jupyter notebooks | [219](https://github.com/laminlabs/nbproject/pull/219) | [Koncopd](https://github.com/Koncopd) | 2022-08-16 | 0.5.0 52 | ♻️ Clean up code | [215](https://github.com/laminlabs/nbproject/pull/215) | [Koncopd](https://github.com/Koncopd) | 2022-08-07 | 53 | ⬆️ Upgrade to logger 0.1.1 & polish `quickstart` | [212](https://github.com/laminlabs/nbproject/pull/212) | [falexwolf](https://github.com/falexwolf) | 2022-07-24 | 54 | 🩹 Safer interactive header update on initialization | [211](https://github.com/laminlabs/nbproject/pull/211) | [Koncopd](https://github.com/Koncopd) | 2022-07-23 | 55 | 🔥 Switched logger to use `lamin_logger` | [210](https://github.com/laminlabs/nbproject/pull/210) | [sunnyosun](https://github.com/sunnyosun) | 2022-07-23 | 56 | 📝 Polish `basic-metadata` and `update-metadata` guide | [209](https://github.com/laminlabs/nbproject/pull/209) | [falexwolf](https://github.com/falexwolf) | 2022-07-23 | 57 | 📝 Add gif to quickstart | [208](https://github.com/laminlabs/nbproject/pull/208) | [falexwolf](https://github.com/falexwolf) | 2022-07-23 | 58 | ♻️ Refactor exposure of `infer_pypackages` | [206](https://github.com/laminlabs/nbproject/pull/206) | [falexwolf](https://github.com/falexwolf) | 2022-07-23 | 59 | 🚸 Add logging to `notebook_path` | [204](https://github.com/laminlabs/nbproject/pull/204) | [Koncopd](https://github.com/Koncopd) | 2022-07-20 | 0.4.5 60 | 🚸 Better logging on no-title error | [203](https://github.com/laminlabs/nbproject/pull/203) | [falexwolf](https://github.com/falexwolf) | 2022-07-20 | 0.4.4 61 | 💄 Allow chaining in `add_pypackages` and prettify logging message | [200](https://github.com/laminlabs/nbproject/pull/200) | [falexwolf](https://github.com/falexwolf) | 2022-07-19 | 62 | ♻️ Add return value for `set_version` | [199](https://github.com/laminlabs/nbproject/pull/199) | [falexwolf](https://github.com/falexwolf) | 2022-07-19 | 0.4.3 63 | ♻️ Refactor and test `set_version` | [198](https://github.com/laminlabs/nbproject/pull/198) | [falexwolf](https://github.com/falexwolf) | 2022-07-19 | 0.4.2 64 | 🚸 Do not raise error upon no-title, return code | [196](https://github.com/laminlabs/nbproject/pull/196) | [falexwolf](https://github.com/falexwolf) | 2022-07-19 | 0.4.1 65 | 💄 Added ✅ for success logging | [197](https://github.com/laminlabs/nbproject/pull/197) | [sunnyosun](https://github.com/sunnyosun) | 2022-07-19 | 66 | 💄 Prettified logging | [195](https://github.com/laminlabs/nbproject/pull/195) | [sunnyosun](https://github.com/sunnyosun) | 2022-07-19 | 67 | 🚸 Show table on initialization, ask to publish if cells not consecutive | [194](https://github.com/laminlabs/nbproject/pull/194) | [Koncopd](https://github.com/Koncopd) | 2022-07-18 | 68 | 💄 Logging with icons | [193](https://github.com/laminlabs/nbproject/pull/193) | [sunnyosun](https://github.com/sunnyosun) | 2022-07-18 | 69 | 🚸 Small changes to header, `add_pypackages` infers versions | [190](https://github.com/laminlabs/nbproject/pull/190) | [Koncopd](https://github.com/Koncopd) | 2022-07-18 | 70 | 💄 Polish docs | [191](https://github.com/laminlabs/nbproject/pull/191) | [falexwolf](https://github.com/falexwolf) | 2022-07-18 | 0.4.0 71 | ✨ Allow passing `pypackage` to `header()`, add consistency checks, and logs for how to update metadata | [188](https://github.com/laminlabs/nbproject/pull/188) | [falexwolf](https://github.com/falexwolf) | 2022-07-18 | 72 | 💚 Fix the 3.7 & 3.8 tests | [186](https://github.com/laminlabs/nbproject/pull/186) | [Koncopd](https://github.com/Koncopd) | 2022-07-18 | 73 | 📝 Simplify guide | [185](https://github.com/laminlabs/nbproject/pull/185) | [falexwolf](https://github.com/falexwolf) | 2022-07-17 | 74 | ✨ Add a parent field to metadata and `header()` | [183](https://github.com/laminlabs/nbproject/pull/183) | [falexwolf](https://github.com/falexwolf) | 2022-07-17 | 75 | 🚚 Rename `dependency` to `pypackage` | [181](https://github.com/laminlabs/nbproject/pull/181) | [falexwolf](https://github.com/falexwolf) | 2022-07-17 | 76 | 💄 Simplify pypackage display & make version the 2nd field to be displayed | [180](https://github.com/laminlabs/nbproject/pull/180) | [falexwolf](https://github.com/falexwolf) | 2022-07-17 | 0.3.2 77 | 👷 Update CI with pyversions, update readme & docs with doi & clean-up | [179](https://github.com/laminlabs/nbproject/pull/179) | [falexwolf](https://github.com/falexwolf) | 2022-07-17 | 78 | ✨ `publish()` and `test_cli` update | [178](https://github.com/laminlabs/nbproject/pull/178) | [Koncopd](https://github.com/Koncopd) | 2022-07-17 | 79 | ♻️ Refactored metadata display | [177](https://github.com/laminlabs/nbproject/pull/177) | [Koncopd](https://github.com/Koncopd) | 2022-07-17 | 80 | ♻️ Small changes for `meta` | [175](https://github.com/laminlabs/nbproject/pull/175) | [Koncopd](https://github.com/Koncopd) | 2022-07-17 | 81 | 🚸 More intuitive auto-lookup and API reference | [173](https://github.com/laminlabs/nbproject/pull/173) | [falexwolf](https://github.com/falexwolf) | 2022-07-16 | 82 | 👷 Introduce compatibility with Python 3.7 | [171](https://github.com/laminlabs/nbproject/pull/171) | [falexwolf](https://github.com/falexwolf) | 2022-07-16 | 0.3.1 83 | ♻️ Refactor `last_cell` check & `consecutiveness` check in `publish()` | [167](https://github.com/laminlabs/nbproject/pull/167) | [falexwolf](https://github.com/falexwolf) | 2022-07-16 | 0.3.0 84 | 🚚 Rename `integrity` to `consecutiveness` | [166](https://github.com/laminlabs/nbproject/pull/166) | [falexwolf](https://github.com/falexwolf) | 2022-07-16 | 85 | 🚸 Remove complexity of Jupyter Lab interaction | [164](https://github.com/laminlabs/nbproject/pull/164) | [falexwolf](https://github.com/falexwolf) | 2022-07-15 | 86 | 🚸 Indent JSON just as editors do | [163](https://github.com/laminlabs/nbproject/pull/163) | [falexwolf](https://github.com/falexwolf) | 2022-07-15 | 87 | 🚸 Raise error upon publishing notebook without title | [161](https://github.com/laminlabs/nbproject/pull/161) | [falexwolf](https://github.com/falexwolf) | 2022-07-14 | 0.2.3 88 | 🚸 Remove execution of cells after metadata write | [159](https://github.com/laminlabs/nbproject/pull/159) | [falexwolf](https://github.com/falexwolf) | 2022-07-14 | 89 | 🚸 Add an uninitialized project for `test_cli` and a small fix to `header` | [154](https://github.com/laminlabs/nbproject/pull/154) | [Koncopd](https://github.com/Koncopd) | 2022-07-13 | 0.2.2 90 | 🚸 Add safety measures, better documentation & logging & tests | [153](https://github.com/laminlabs/nbproject/pull/153) | [falexwolf](https://github.com/falexwolf) | 2022-07-13 | 91 | 🚸 Clearer logging output upon init in VS Code and Classic Notebook | [152](https://github.com/laminlabs/nbproject/pull/152) | [falexwolf](https://github.com/falexwolf) | 2022-07-13 | 92 | 🔥 Remove automatically calling `header()` upon import | [151](https://github.com/laminlabs/nbproject/pull/151) | [falexwolf](https://github.com/falexwolf) | 2022-07-13 | 93 | ♻️ Enable passing the calling_code statement to publish | [148](https://github.com/laminlabs/nbproject/pull/148) | [falexwolf](https://github.com/falexwolf) | 2022-07-12 | 0.2.1 94 | ✨ Add last cell check to publish | [143](https://github.com/laminlabs/nbproject/pull/143) | [Koncopd](https://github.com/Koncopd) | 2022-07-11 | 95 | 📝 Add header() call to every notebook | [141](https://github.com/laminlabs/nbproject/pull/141) | [falexwolf](https://github.com/falexwolf) | 2022-07-11 | 0.2.0 96 | 🚸 Add `i_confirm_i_saved` arg to `publish()` for usage outside Jupyter Lab | [140](https://github.com/laminlabs/nbproject/pull/140) | [falexwolf](https://github.com/falexwolf) | 2022-07-11 | 97 | 🚚 Migrated test submodule to `nbproject_test` | [138](https://github.com/laminlabs/nbproject/pull/138) | [falexwolf](https://github.com/falexwolf) | 2022-07-11 | 98 | 🚚 Remove mention of footer, lock in publish | [137](https://github.com/laminlabs/nbproject/pull/137) | [falexwolf](https://github.com/falexwolf) | 2022-07-11 | 99 | 📝 Add a nutshell tutorial to demo canonical workflow | [135](https://github.com/laminlabs/nbproject/pull/135) | [falexwolf](https://github.com/falexwolf) | 2022-07-10 | 100 | 🚸 Turn `header()` from pseudo-module into function, remove `Header` class | [133](https://github.com/laminlabs/nbproject/pull/133) | [falexwolf](https://github.com/falexwolf) | 2022-07-10 | 101 | ✅ Improve coverage | [130](https://github.com/laminlabs/nbproject/pull/130) | [Koncopd](https://github.com/Koncopd) | 2022-07-10 | 102 | 🚸 More intuitive API for updating pypackage store and writing it to file | [126](https://github.com/laminlabs/nbproject/pull/126) | [falexwolf](https://github.com/falexwolf) | 2022-07-09 | 103 | ✨ Add a more convenient way of updating the pypackage store | [125](https://github.com/laminlabs/nbproject/pull/125) | [Koncopd](https://github.com/Koncopd) | 2022-07-09 | 104 | 💄 Prettify logging during testing | [124](https://github.com/laminlabs/nbproject/pull/124) | [falexwolf](https://github.com/falexwolf) | 2022-07-09 | 105 | 📝 Add a published notebook | [120](https://github.com/laminlabs/nbproject/pull/120) | [falexwolf](https://github.com/falexwolf) | 2022-07-05 | 106 | ✅ Improve notebook testing API | [116](https://github.com/laminlabs/nbproject/pull/116) | [Koncopd](https://github.com/Koncopd) | 2022-07-03 | 0.1.6 107 | ✨ Publish notebooks from the CLI | [112](https://github.com/laminlabs/nbproject/pull/112) | [Koncopd](https://github.com/Koncopd) | 2022-07-01 | 108 | 💄 Avoid non-JupyterLab env logging messages in CI env | [114](https://github.com/laminlabs/nbproject/pull/114) | [falexwolf](https://github.com/falexwolf) | 2022-07-01 | 0.1.5 109 | ✅ Execute notebooks in order of index | [113](https://github.com/laminlabs/nbproject/pull/113) | [falexwolf](https://github.com/falexwolf) | 2022-07-01 | 110 | 💄 More compact logging and other small fixes | [111](https://github.com/laminlabs/nbproject/pull/111) | [falexwolf](https://github.com/falexwolf) | 2022-07-01 | 0.1.4 111 | ♻️ Refactor meta and header | [110](https://github.com/laminlabs/nbproject/pull/110) | [Koncopd](https://github.com/Koncopd) | 2022-07-01 | 112 | 🚸 Polish publishing experience | [commit](https://github.com/laminlabs/nbproject/commit/dec15d05bcf3cdf17498fc1a164a29765d48a2e3) | [falexwolf](https://github.com/falexwolf) | 2022-07-01 | 0.1.3 113 | 🚸 Improve user experience during publishing | [109](https://github.com/laminlabs/nbproject/pull/109) | [falexwolf](https://github.com/falexwolf) | 2022-07-01 | 0.1.2 114 | ✨ Add VS Code integration | [107](https://github.com/laminlabs/nbproject/pull/107) | [falexwolf](https://github.com/falexwolf) | 2022-06-29 | 0.1.1 115 | ✨ Implement the publish function | [106](https://github.com/laminlabs/nbproject/pull/106) | [Koncopd](https://github.com/Koncopd) | 2022-06-29 | 116 | 📝 Add proper guide | [105](https://github.com/laminlabs/nbproject/pull/105) | [falexwolf](https://github.com/falexwolf) | 2022-06-29 | 117 | 📝 Prototype the `nbproject.publish` workflow | [103](https://github.com/laminlabs/nbproject/pull/103) | [falexwolf](https://github.com/falexwolf) | 2022-06-29 | 118 | ✨ Take into account packages from store for live pypackages | [104](https://github.com/laminlabs/nbproject/pull/104) | [Koncopd](https://github.com/Koncopd) | 2022-06-29 | 119 | ✨ Add field `version` and display live pypackages in header | [102](https://github.com/laminlabs/nbproject/pull/102) | [falexwolf](https://github.com/falexwolf) | 2022-06-29 | 120 | 👷 Update coverage CI setup | [101](https://github.com/laminlabs/nbproject/pull/101) | [falexwolf](https://github.com/falexwolf) | 2022-06-26 | 121 | 👷 Integrate Codecov to CI | [100](https://github.com/laminlabs/nbproject/pull/100) | [sunnyosun](https://github.com/sunnyosun) | 2022-06-26 | 122 | 📝 Re-organize API documentation and clean up faq | [97](https://github.com/laminlabs/nbproject/pull/97) | [falexwolf](https://github.com/falexwolf) | 2022-06-24 | 0.1.0 123 | ♻️ Restructure dev submodule | [93](https://github.com/laminlabs/nbproject/pull/93) | [Koncopd](https://github.com/Koncopd) | 2022-06-23 | 0.1a3 124 | 🐛 Fix the case when a notebook filename is specified | [94](https://github.com/laminlabs/nbproject/pull/94) | [Koncopd](https://github.com/Koncopd) | 2022-06-23 | 0.1a2 125 | ♻️ Re-design package and introduce large parts of the API | [90](https://github.com/laminlabs/nbproject/pull/90) | [Koncopd](https://github.com/Koncopd) | 2022-06-21 | 0.1a1 126 | 🧑‍💻 Make CLI `--deps` infer versions by default | [87](https://github.com/laminlabs/nbproject/pull/87) | [Koncopd](https://github.com/Koncopd) | 2022-06-12 | 127 | 🩹 Always display pypackages pinned in the metadata | [84](https://github.com/laminlabs/nbproject/pull/84) | [Koncopd](https://github.com/Koncopd) | 2022-06-10 | 128 | 💚 Remove server initialization from tests | [83](https://github.com/laminlabs/nbproject/pull/83) | [Koncopd](https://github.com/Koncopd) | 2022-06-09 | 129 | 🏗️ Always infer & display pypackages | [80](https://github.com/laminlabs/nbproject/pull/80) | [falexwolf](https://github.com/falexwolf) | 2022-06-08 | 0.0.9 130 | 🩹 Fix `meta.title` formatting | [79](https://github.com/laminlabs/nbproject/pull/79) | [falexwolf](https://github.com/falexwolf) | 2022-06-08 | 131 | ✨ Expose title in `nbproject.meta` & refactor to loading dynamically | [77](https://github.com/laminlabs/nbproject/pull/77) | [falexwolf](https://github.com/falexwolf) | 2022-06-08 | 0.0.8 132 | ♻️ Rename `meta.uid` to `meta.id` | [75](https://github.com/laminlabs/nbproject/pull/75) | [falexwolf](https://github.com/falexwolf) | 2022-06-08 133 | ✨ Infer pypackages on header import | [65](https://github.com/laminlabs/nbproject/pull/65) | [Koncopd](https://github.com/Koncopd) | 2022-06-07 | 134 | 🏗️ Access metadata through API | [59](https://github.com/laminlabs/nbproject/pull/59) | [falexwolf](https://github.com/falexwolf) | 2022-06-06 | 135 | ✨ Upon interactive init, auto-restart & run | [57](https://github.com/laminlabs/nbproject/pull/57) | [falexwolf](https://github.com/falexwolf) | 2022-06-04 | 136 | ♻️ Replace nbformat with orjson everywhere | [50](https://github.com/laminlabs/nbproject/pull/50) | [Koncopd](https://github.com/Koncopd) | 2022-05-31 | 137 | ⚡ Improve runtime for `nbproject.header` | [47](https://github.com/laminlabs/nbproject/pull/47) | [Koncopd](https://github.com/Koncopd) | 2022-05-23 | 138 | ♻️ Proper treatment of jupyter magic commands | [48](https://github.com/laminlabs/nbproject/pull/48) | [Koncopd](https://github.com/Koncopd) | 2022-05-23 | 139 | 💚 Safer pytest run | [41](https://github.com/laminlabs/nbproject/pull/41) | [Koncopd](https://github.com/Koncopd) | 2022-05-15 | 140 | ✨ Add pypackages management | [26](https://github.com/laminlabs/nbproject/pull/26) | [Koncopd](https://github.com/Koncopd) | 2022-05-13 | 141 | ✨ Project initialization with CLI | [21](https://github.com/laminlabs/nbproject/pull/21) | [Koncopd](https://github.com/Koncopd) | 2022-04-29 | 142 | 🎨 Move all jupyter related functionality to one file | [19](https://github.com/laminlabs/nbproject/pull/19) | [Koncopd](https://github.com/Koncopd) | 2022-04-18 | 143 | 🏗️ Metadata-aware tests | [9](https://github.com/laminlabs/nbproject/pull/9) | [Koncopd](https://github.com/Koncopd) | 2022-04-07 | 144 | 🏗️ Metadata-aware tests (take 1) | [9](https://github.com/laminlabs/nbproject/pull/9) | [Koncopd](https://github.com/Koncopd) | 2022-04-07 | 145 | ✨ Add `time_init` & `time_edit` fields | [6](https://github.com/laminlabs/nbproject/pull/6) | [falexwolf](https://github.com/falexwolf) | 2022-03-23 | 146 | 💄 Display only 4 digits, use pydantic, autodetect filename | [5](https://github.com/laminlabs/nbproject/pull/5) | [falexwolf](https://github.com/falexwolf) | 2022-03-23 | 147 | 📝 Update readme | [2](https://github.com/laminlabs/nbproject/pull/2) | [falexwolf](https://github.com/falexwolf) | 2022-03-23 | 148 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | Setup & installation guide: 4 | 5 | - {doc}`faq/setup` 6 | 7 | Edges cases, warnings, and errors: 8 | 9 | - {doc}`faq/initialize` 10 | - {doc}`faq/initialize-set-env` 11 | - {doc}`faq/inconsistent-packages-parents-store` 12 | - {doc}`faq/inconsistent-packages-parents-no-store` 13 | - {doc}`faq/example-project` 14 | - {doc}`faq/example-project-uninitialized` 15 | - {doc}`faq/example-after-init-set-filename` 16 | - {doc}`faq/publish-set-version` 17 | - [Publish without title](faq/publish-without-title) 18 | - {doc}`faq/publish-without-saving` 19 | - {doc}`faq/publish-not-last-cell` 20 | - {doc}`faq/title-not-at-top` 21 | - {doc}`faq/publish-wrapper` 22 | - {doc}`faq/trigger-exit-upon-init` 23 | - {doc}`faq/not-initialized` 24 | - {doc}`faq/internal-functions` 25 | - {doc}`faq/set-env-via-environment-var` 26 | - {doc}`faq/header-author-field` 27 | - {doc}`faq/cell-source-string` 28 | 29 | ```{toctree} 30 | :maxdepth: 1 31 | :hidden: 32 | 33 | faq/setup 34 | faq/initialize 35 | faq/initialize-set-env 36 | faq/inconsistent-packages-parents-store 37 | faq/inconsistent-packages-parents-no-store 38 | faq/example-project 39 | faq/example-project-uninitialized 40 | faq/example-after-init-set-filename 41 | faq/publish-set-version 42 | faq/publish-without-title 43 | faq/publish-without-saving 44 | faq/publish-not-last-cell 45 | faq/publish-wrapper 46 | faq/title-not-at-top 47 | faq/trigger-exit-upon-init 48 | faq/not-initialized 49 | faq/internal-functions 50 | faq/set-env-via-environment-var 51 | faq/header-author-field 52 | faq/cell-source-string 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/faq/cell-source-string.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "65e19088d582d71d", 6 | "metadata": {}, 7 | "source": "# Check with cell sources being strings" 8 | }, 9 | { 10 | "cell_type": "code", 11 | "execution_count": null, 12 | "id": "3a01a044c3767dc9", 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": "import nbproject" 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "id": "7051321d7eb8c452", 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": "assert nbproject.meta.live.title == \"Check with cell sources being strings\"" 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "id": "b2d5e63449430ff8", 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": "assert nbproject.meta.live.consecutive_cells" 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "d32912abaefbdc13", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": "" 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 2 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython2", 58 | "version": "2.7.6" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 5 63 | } 64 | -------------------------------------------------------------------------------- /docs/faq/example-after-init-set-filename.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "cc6c2fe6-6291-4a91-b326-b1ec6cb8d703", 6 | "metadata": { 7 | "tags": [] 8 | }, 9 | "source": [ 10 | "# Manually specify a filepath upon init" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "752e0baa-5e48-46c0-958d-f48796ff3b2f", 16 | "metadata": {}, 17 | "source": [ 18 | "Here we see how `nbproject` prints a metadata header for a notebook that has been initialized." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "705ac1d5-cbd9-421f-841e-d91e15faa962", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "from nbproject import header\n", 29 | "\n", 30 | "header(filepath=\"example-after-init-set-filename.ipynb\")" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "861ede19-560c-4d7f-9b64-10728eef4ef6", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "from nbproject import meta" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": null, 46 | "id": "170ff0fd-730e-4b6b-b932-d396fa2d82d2", 47 | "metadata": { 48 | "tags": [ 49 | "hide-cell" 50 | ] 51 | }, 52 | "outputs": [], 53 | "source": [ 54 | "assert meta.store.id == \"KLqggebgs7Tq\"\n", 55 | "assert hasattr(meta.store, \"time_init\")" 56 | ] 57 | } 58 | ], 59 | "metadata": { 60 | "interpreter": { 61 | "hash": "40d3a090f54c6569ab1632332b64b2c03c39dcf918b08424e98f38b5ae0af88f" 62 | }, 63 | "kernelspec": { 64 | "display_name": "Python 3 (ipykernel)", 65 | "language": "python", 66 | "name": "python3" 67 | }, 68 | "language_info": { 69 | "codemirror_mode": { 70 | "name": "ipython", 71 | "version": 3 72 | }, 73 | "file_extension": ".py", 74 | "mimetype": "text/x-python", 75 | "name": "python", 76 | "nbconvert_exporter": "python", 77 | "pygments_lexer": "ipython3", 78 | "version": "3.9.7" 79 | }, 80 | "nbproject": { 81 | "id": "KLqggebgs7Tq", 82 | "time_init": "2022-04-18T22:07:44.214819+00:00", 83 | "version": "0" 84 | } 85 | }, 86 | "nbformat": 4, 87 | "nbformat_minor": 5 88 | } 89 | -------------------------------------------------------------------------------- /docs/faq/example-project-uninitialized.md: -------------------------------------------------------------------------------- 1 | # An example uninitialized project 2 | 3 | There is just a single notebook here right now. 4 | 5 | ```{toctree} 6 | :glob: 7 | :reversed: 8 | 9 | example-project-uninitialized/* 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/faq/example-project-uninitialized/2022-07-13-my-task-x.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1db96b40", 6 | "metadata": {}, 7 | "source": [ 8 | "# My task X" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "0a17df65", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "%%time\n", 19 | "from nbproject import header\n", 20 | "\n", 21 | "header()" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "f5216382", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "import pandas as pd\n", 32 | "import pydantic" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "b630b839", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "df = pd.DataFrame({\"a\": [1, 2, 3], \"b\": [2, 3, 4]})" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "id": "4e1eb065", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "df" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "id": "64a0102d", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "df.mean()" 63 | ] 64 | } 65 | ], 66 | "metadata": { 67 | "kernelspec": { 68 | "display_name": "Python 3 (ipykernel)", 69 | "language": "python", 70 | "name": "python3" 71 | }, 72 | "language_info": { 73 | "codemirror_mode": { 74 | "name": "ipython", 75 | "version": 3 76 | }, 77 | "file_extension": ".py", 78 | "mimetype": "text/x-python", 79 | "name": "python", 80 | "nbconvert_exporter": "python", 81 | "pygments_lexer": "ipython3", 82 | "version": "3.9.12" 83 | } 84 | }, 85 | "nbformat": 4, 86 | "nbformat_minor": 5 87 | } 88 | -------------------------------------------------------------------------------- /docs/faq/example-project.md: -------------------------------------------------------------------------------- 1 | # An example project 2 | 3 | There are two notebooks here right now. 4 | 5 | ```{toctree} 6 | :glob: 7 | :reversed: 8 | 9 | example-project/* 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/faq/example-project/2022-05-13-my-task-x.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1d7b3b73-75f9-468d-8783-873ebadcab9b", 6 | "metadata": {}, 7 | "source": [ 8 | "# My task X" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "97ec6631-8473-4a2d-b488-def921bb83de", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "%%time\n", 19 | "from nbproject import header\n", 20 | "\n", 21 | "header()" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "cfbbaf21-ec90-4be4-b25f-8a3d51ef535d", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "import pandas as pd\n", 32 | "import pydantic" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "id": "b430b7f1-d688-4c67-bf3d-f2c851eee64e", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "df = pd.DataFrame({\"a\": [1, 2, 3], \"b\": [2, 3, 4]})" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "id": "32fc6616-9263-4817-b4ad-70a6c272f720", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "df" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "id": "0ae9a2f6-366e-40cb-a37f-f5172401aa3d", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "df.mean()" 63 | ] 64 | } 65 | ], 66 | "metadata": { 67 | "kernelspec": { 68 | "display_name": "Python 3 (ipykernel)", 69 | "language": "python", 70 | "name": "python3" 71 | }, 72 | "language_info": { 73 | "codemirror_mode": { 74 | "name": "ipython", 75 | "version": 3 76 | }, 77 | "file_extension": ".py", 78 | "mimetype": "text/x-python", 79 | "name": "python", 80 | "nbconvert_exporter": "python", 81 | "pygments_lexer": "ipython3", 82 | "version": "3.9.12" 83 | }, 84 | "nbproject": { 85 | "id": "ZGhPhKR2SkXQ", 86 | "pypackage": { 87 | "nbproject": "0.0.5+3.g8b2e3e2", 88 | "pandas": "1.4.2", 89 | "pydantic": "1.9.0" 90 | }, 91 | "time_init": "2022-05-14T19:15:33.451107+00:00", 92 | "version": "0" 93 | } 94 | }, 95 | "nbformat": 4, 96 | "nbformat_minor": 5 97 | } 98 | -------------------------------------------------------------------------------- /docs/faq/example-project/2022-07-18-my-task-y.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "23791b5d", 6 | "metadata": {}, 7 | "source": [ 8 | "# My task Y" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "82f7ed3a", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "%%time\n", 19 | "from nbproject import header\n", 20 | "\n", 21 | "header()" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "556b80b4", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "import pandas as pd" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "062e4cf5", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "df = pd.DataFrame({\"a\": [1, 2, 3], \"b\": [2, 3, 4]})" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "id": "d94a8d94", 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "df" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "id": "a4e9d74e", 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "df.min()" 62 | ] 63 | } 64 | ], 65 | "metadata": { 66 | "kernelspec": { 67 | "display_name": "Python 3 (ipykernel)", 68 | "language": "python", 69 | "name": "python3" 70 | }, 71 | "language_info": { 72 | "codemirror_mode": { 73 | "name": "ipython", 74 | "version": 3 75 | }, 76 | "file_extension": ".py", 77 | "mimetype": "text/x-python", 78 | "name": "python", 79 | "nbconvert_exporter": "python", 80 | "pygments_lexer": "ipython3", 81 | "version": "3.9.12" 82 | }, 83 | "nbproject": { 84 | "id": "RsTp3QoTb68g", 85 | "pypackage": { 86 | "nbproject": "0+untagged.34.g096f461", 87 | "pandas": "1.4.2" 88 | }, 89 | "time_init": "2022-07-18T15:22:49.697754+00:00", 90 | "version": "0" 91 | } 92 | }, 93 | "nbformat": 4, 94 | "nbformat_minor": 5 95 | } 96 | -------------------------------------------------------------------------------- /docs/faq/example-project/nbproject_metadata.yml: -------------------------------------------------------------------------------- 1 | ZGhPhKR2SkXQ: 2 | time_init: 2022-05-14 19:15 3 | name: 2022-05-13-my-task-x.ipynb 4 | version: 0 5 | location: . 6 | pypackage: 7 | nbproject: 0.0.5+3.g8b2e3e2 8 | pandas: 1.4.2 9 | pydantic: 1.9.0 10 | RsTp3QoTb68g: 11 | time_init: 2022-07-18 15:22 12 | name: 2022-07-18-my-task-y.ipynb 13 | version: 0 14 | location: . 15 | pypackage: 16 | nbproject: 0+untagged.34.g096f461 17 | pandas: 1.4.2 18 | -------------------------------------------------------------------------------- /docs/faq/header-author-field.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1bead9ef", 6 | "metadata": {}, 7 | "source": [ 8 | "# Author field in `header` and `meta.store`" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "4a2ddf26", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from nbproject import header, meta" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "id": "8b390071", 24 | "metadata": {}, 25 | "source": [ 26 | "If a `lamindb` user account was [set up](https://lamin.ai/docs/db/guide/get-started), then initialized headers of notebooks will have an author field consisting of `user_handle` and `user_id` in brackets. Here they are the same." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "id": "d25474c3", 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "header()" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "id": "031af731", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "meta" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "id": "4bc94a37", 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "assert meta.store.user_handle == \"qTQ5q0ar\"\n", 57 | "assert meta.store.user_id == \"qTQ5q0ar\"" 58 | ] 59 | } 60 | ], 61 | "metadata": { 62 | "kernelspec": { 63 | "display_name": "Python 3 (ipykernel)", 64 | "language": "python", 65 | "name": "python3" 66 | }, 67 | "language_info": { 68 | "codemirror_mode": { 69 | "name": "ipython", 70 | "version": 3 71 | }, 72 | "file_extension": ".py", 73 | "mimetype": "text/x-python", 74 | "name": "python", 75 | "nbconvert_exporter": "python", 76 | "pygments_lexer": "ipython3", 77 | "version": "3.9.12" 78 | }, 79 | "nbproject": { 80 | "id": "Zv1wV7oM9ztH", 81 | "parent": null, 82 | "pypackage": null, 83 | "time_init": "2022-10-09T15:08:04.997800+00:00", 84 | "user_handle": "qTQ5q0ar", 85 | "user_id": "qTQ5q0ar", 86 | "version": "0" 87 | } 88 | }, 89 | "nbformat": 4, 90 | "nbformat_minor": 5 91 | } 92 | -------------------------------------------------------------------------------- /docs/faq/inconsistent-packages-parents-no-store.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Passing packages or parents when there is no store" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from nbproject import header" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "Consistent parents:" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "header(parent=[\"z14KWQKD4bwE\", \"jhvyoIrxoeSz\"])" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "Inconsistent packages:" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "header(pypackage=\"nbformat\")" 49 | ] 50 | } 51 | ], 52 | "metadata": { 53 | "kernelspec": { 54 | "display_name": "Python 3 (ipykernel)", 55 | "language": "python", 56 | "name": "python3" 57 | }, 58 | "language_info": { 59 | "codemirror_mode": { 60 | "name": "ipython", 61 | "version": 3 62 | }, 63 | "file_extension": ".py", 64 | "mimetype": "text/x-python", 65 | "name": "python", 66 | "nbconvert_exporter": "python", 67 | "pygments_lexer": "ipython3", 68 | "version": "3.9.12" 69 | }, 70 | "nbproject": { 71 | "id": "3m2Q6UBuwgSH", 72 | "time_init": "2022-07-18T12:32:21.625107+00:00", 73 | "version": "0" 74 | }, 75 | "vscode": { 76 | "interpreter": { 77 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 78 | } 79 | } 80 | }, 81 | "nbformat": 4, 82 | "nbformat_minor": 4 83 | } 84 | -------------------------------------------------------------------------------- /docs/faq/inconsistent-packages-parents-store.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Passing inconsistent packages or parents when there is a store" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from nbproject import header" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "Consistent parents:" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "header(parent=[\"z14KWQKD4bwE\", \"jhvyoIrxoeSz\"])" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "Dropping one parent:" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "header(parent=\"z14KWQKD4bwE\")" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "Wrong parents:" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "header(parent=\"a\")" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "header(parent=[\"z14KWQKD4bwE\", \"a\"])" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "Inconsistent packages:" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "header(pypackage=\"nbformat\")" 90 | ] 91 | } 92 | ], 93 | "metadata": { 94 | "kernelspec": { 95 | "display_name": "Python 3 (ipykernel)", 96 | "language": "python", 97 | "name": "python3" 98 | }, 99 | "language_info": { 100 | "codemirror_mode": { 101 | "name": "ipython", 102 | "version": 3 103 | }, 104 | "file_extension": ".py", 105 | "mimetype": "text/x-python", 106 | "name": "python", 107 | "nbconvert_exporter": "python", 108 | "pygments_lexer": "ipython3", 109 | "version": "3.9.12" 110 | }, 111 | "nbproject": { 112 | "id": "3m2Q6UBuwgSH", 113 | "parent": [ 114 | "z14KWQKD4bwE", 115 | "jhvyoIrxoeSz" 116 | ], 117 | "pypackage": { 118 | "nbproject": "0.1.6", 119 | "pandas": "1.4.1" 120 | }, 121 | "time_init": "2022-07-18T12:32:21.625107+00:00", 122 | "version": "0" 123 | }, 124 | "vscode": { 125 | "interpreter": { 126 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 127 | } 128 | } 129 | }, 130 | "nbformat": 4, 131 | "nbformat_minor": 4 132 | } 133 | -------------------------------------------------------------------------------- /docs/faq/initialize-set-env.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "a96f0c2b", 6 | "metadata": {}, 7 | "source": [ 8 | "# Set env, initialize and publish" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "33cd72dc", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from nbproject import header, meta, publish" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "546adf14", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "header(env=\"notebook\")" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "id": "5d6996c5", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "assert meta.env == \"notebook\"" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "3585eab4", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "publish(proceed_consecutiveness=\"y\")" 49 | ] 50 | } 51 | ], 52 | "metadata": { 53 | "kernelspec": { 54 | "display_name": "Python 3 (ipykernel)", 55 | "language": "python", 56 | "name": "python3" 57 | }, 58 | "language_info": { 59 | "codemirror_mode": { 60 | "name": "ipython", 61 | "version": 3 62 | }, 63 | "file_extension": ".py", 64 | "mimetype": "text/x-python", 65 | "name": "python", 66 | "nbconvert_exporter": "python", 67 | "pygments_lexer": "ipython3", 68 | "version": "3.9.12" 69 | } 70 | }, 71 | "nbformat": 4, 72 | "nbformat_minor": 5 73 | } 74 | -------------------------------------------------------------------------------- /docs/faq/initialize.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Initialize nbproject for your notebook" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "To initialize nbproject, call {func}`nbproject.header`. For example, by adding `from nbproject import header; header()` to the first code cell:" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "Jupyter Lab | VS Code | Classic Notebook\n", 22 | "--- | --- | ---\n", 23 | "Displays \"Initializing.\" and requires no further action. | \"image\" | \"image\"" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "```{important}\n", 31 | "\n", 32 | "- You'll have the best interactive experience in **Jupyter Lab**.\n", 33 | "- In other editors, you'll need to manually hit save and reload the changed `.ipynb` file from disk (discard the editor content).\n", 34 | "\n", 35 | "We recommend you use Jupyter Lab for a seamless interactive experience.\n", 36 | "\n", 37 | "```" 38 | ] 39 | } 40 | ], 41 | "metadata": { 42 | "kernelspec": { 43 | "display_name": "Python 3.9.12 ('base1')", 44 | "language": "python", 45 | "name": "python3" 46 | }, 47 | "language_info": { 48 | "codemirror_mode": { 49 | "name": "ipython", 50 | "version": 3 51 | }, 52 | "file_extension": ".py", 53 | "mimetype": "text/x-python", 54 | "name": "python", 55 | "nbconvert_exporter": "python", 56 | "pygments_lexer": "ipython3", 57 | "version": "3.9.12" 58 | }, 59 | "vscode": { 60 | "interpreter": { 61 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 62 | } 63 | } 64 | }, 65 | "nbformat": 4, 66 | "nbformat_minor": 4 67 | } 68 | -------------------------------------------------------------------------------- /docs/faq/internal-functions.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "07b4416e", 6 | "metadata": {}, 7 | "source": [ 8 | "# Check internal functions" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "dec58684", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from nbproject import header" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "33d06aa6", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "header()" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "id": "fd29b2f1", 34 | "metadata": {}, 35 | "source": [ 36 | "Utilities to communicate with jupyter frontend." 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "id": "d2c597ec", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "from nbproject.dev._frontend_commands import _save_notebook, _reload_notebook" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "id": "c69d273f", 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "from nbproject.dev._jupyter_lab_commands import _lab_notebook_path" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "id": "3f822cb7", 62 | "metadata": {}, 63 | "source": [ 64 | "Here nothing should happen in the test environment." 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "id": "46d7daac", 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "_save_notebook(env=\"lab\")\n", 75 | "_save_notebook(env=\"notebook\")" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "id": "0673cc01", 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [ 85 | "_reload_notebook(env=\"lab\")\n", 86 | "_reload_notebook(env=\"notebook\")" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "id": "d6578ad9", 92 | "metadata": {}, 93 | "source": [ 94 | "Can't get the path through ipylab, because we are not in juypter lab." 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "id": "a4c9453d", 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "assert _lab_notebook_path() is None" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "id": "28bc688c", 110 | "metadata": {}, 111 | "source": [ 112 | "Utilities to communicate with jupyter backend." 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "id": "1830c1ed", 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "from nbproject.dev._jupyter_communicate import (\n", 123 | " notebook_path,\n", 124 | " prepare_url,\n", 125 | " query_server,\n", 126 | " running_servers,\n", 127 | ")" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "id": "778b1e93", 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "servers_nbapp, servers_juserv = running_servers()\n", 138 | "\n", 139 | "assert list(servers_nbapp) == []\n", 140 | "assert list(servers_juserv) == []" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "id": "2f094485", 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "server = dict(token=\"test\", url=\"localhost/\")" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "id": "310c4e79", 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "assert (\n", 161 | " prepare_url(server, \"/test_query\")\n", 162 | " == \"localhost/api/sessions/test_query?token=test\" # noqa\n", 163 | ")" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "id": "5b7ba29c", 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "from pytest import raises\n", 174 | "\n", 175 | "with raises(Exception):\n", 176 | " query_server(server)" 177 | ] 178 | } 179 | ], 180 | "metadata": { 181 | "kernelspec": { 182 | "display_name": "Python 3 (ipykernel)", 183 | "language": "python", 184 | "name": "python3" 185 | }, 186 | "language_info": { 187 | "codemirror_mode": { 188 | "name": "ipython", 189 | "version": 3 190 | }, 191 | "file_extension": ".py", 192 | "mimetype": "text/x-python", 193 | "name": "python", 194 | "nbconvert_exporter": "python", 195 | "pygments_lexer": "ipython3", 196 | "version": "3.9.12" 197 | }, 198 | "nbproject": { 199 | "id": "IyjiKeXJhQhA", 200 | "parent": null, 201 | "pypackage": null, 202 | "time_init": "2022-08-25T18:37:39.657360+00:00", 203 | "version": "0" 204 | } 205 | }, 206 | "nbformat": 4, 207 | "nbformat_minor": 5 208 | } 209 | -------------------------------------------------------------------------------- /docs/faq/not-initialized.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "26e6045d-f4e7-45a9-b6fb-4641cc0d6f86", 6 | "metadata": {}, 7 | "source": [ 8 | "# Access metadata of un-initialized notebook" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "13774927-ff59-40c1-888a-abec89465244", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from nbproject import meta" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "cf19c0f9-8f4e-4451-8a09-e78a7da550c5", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "meta.store.id" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "id": "2bfbbe72-70a7-45f1-80e4-79021b2dd761", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "meta.store.time_init" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "cdafba5c-226d-47cd-a79b-337928ab0d31", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "meta.store.version" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "id": "fbcc2e26-181d-4568-80e0-b6bf51dba86d", 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "assert meta.store.id == \"not initialized\"\n", 59 | "assert meta.store.time_init == \"not initialized\"\n", 60 | "assert meta.store.version == \"not initialized\"" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "id": "9ed861eb-874b-4409-b339-6516ff66d898", 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "meta.live.time_run" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "id": "cb466e13-b03f-4b86-8b1e-350596cd9734", 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "meta.live.time_passed" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "id": "349d5652-f9bb-408f-b68f-e98caba0461c", 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "meta.live.consecutive_cells" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "id": "4ce85093-6b5a-4616-8c23-d8b972707314", 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "meta.live.pypackage" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "id": "a6e24832-3103-4b0b-a808-0c23ef820525", 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "meta.live.title" 111 | ] 112 | } 113 | ], 114 | "metadata": { 115 | "kernelspec": { 116 | "display_name": "Python 3 (ipykernel)", 117 | "language": "python", 118 | "name": "python3" 119 | }, 120 | "language_info": { 121 | "codemirror_mode": { 122 | "name": "ipython", 123 | "version": 3 124 | }, 125 | "file_extension": ".py", 126 | "mimetype": "text/x-python", 127 | "name": "python", 128 | "nbconvert_exporter": "python", 129 | "pygments_lexer": "ipython3", 130 | "version": "3.9.12" 131 | } 132 | }, 133 | "nbformat": 4, 134 | "nbformat_minor": 5 135 | } 136 | -------------------------------------------------------------------------------- /docs/faq/publish-not-last-cell.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "2f91595e-231a-4be8-b405-582c8ba4b948", 6 | "metadata": {}, 7 | "source": [ 8 | "# Publish not from the last code cell" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "ae93af3f-1e6a-4d2f-b3b0-fdc845b50d0e", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from nbproject import header, publish\n", 19 | "\n", 20 | "header()" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "662b93db-b40a-4fa5-95fb-f22ddca3d309", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import pytest" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "id": "0eb38aa5-35c6-4079-985a-b91e4859a8de", 36 | "metadata": {}, 37 | "source": [ 38 | "The following will throw an error, because publish is not in the last code cell." 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "e6943826-f6ac-414a-b3b2-e4f8af86823b", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "with pytest.raises(RuntimeError):\n", 49 | " publish(i_confirm_i_saved=True)" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "id": "c8a637c0", 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "1 + 1" 60 | ] 61 | } 62 | ], 63 | "metadata": { 64 | "kernelspec": { 65 | "display_name": "Python 3 (ipykernel)", 66 | "language": "python", 67 | "name": "python3" 68 | }, 69 | "language_info": { 70 | "codemirror_mode": { 71 | "name": "ipython", 72 | "version": 3 73 | }, 74 | "file_extension": ".py", 75 | "mimetype": "text/x-python", 76 | "name": "python", 77 | "nbconvert_exporter": "python", 78 | "pygments_lexer": "ipython3", 79 | "version": "3.9.12" 80 | }, 81 | "nbproject": { 82 | "id": "VahIVDKKScdz", 83 | "pypackage": null, 84 | "time_init": "2022-07-11T12:16:53.444875+00:00", 85 | "version": "0" 86 | } 87 | }, 88 | "nbformat": 4, 89 | "nbformat_minor": 5 90 | } 91 | -------------------------------------------------------------------------------- /docs/faq/publish-set-version.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "00178cd6-39c2-499b-969d-1f547a586124", 6 | "metadata": {}, 7 | "source": [ 8 | "# Publish with manually setting a version" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "aed6d5aa-3825-4b82-bdc1-cd973c9fea98", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from nbproject import header, publish\n", 19 | "\n", 20 | "header()" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "a61b5e1f-1556-4a98-814a-a3a0e0fa9a03", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "print(\"hello world\")" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "adb818bf-68a3-439f-95ed-2bbd56a682f3", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "publish(version=\"1.2\")" 41 | ] 42 | } 43 | ], 44 | "metadata": { 45 | "kernelspec": { 46 | "display_name": "Python 3 (ipykernel)", 47 | "language": "python", 48 | "name": "python3" 49 | }, 50 | "language_info": { 51 | "codemirror_mode": { 52 | "name": "ipython", 53 | "version": 3 54 | }, 55 | "file_extension": ".py", 56 | "mimetype": "text/x-python", 57 | "name": "python", 58 | "nbconvert_exporter": "python", 59 | "pygments_lexer": "ipython3", 60 | "version": "3.9.12" 61 | }, 62 | "nbproject": { 63 | "id": "I5wv6FHY2lUH", 64 | "pypackage": null, 65 | "time_init": "2022-07-16T18:36:12.828570+00:00", 66 | "version": "0" 67 | } 68 | }, 69 | "nbformat": 4, 70 | "nbformat_minor": 5 71 | } 72 | -------------------------------------------------------------------------------- /docs/faq/publish-without-saving.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "2f91595e-231a-4be8-b405-582c8ba4b948", 6 | "metadata": {}, 7 | "source": [ 8 | "# Publish without confirming saving outside Jupyter Lab" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "ae93af3f-1e6a-4d2f-b3b0-fdc845b50d0e", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from nbproject import header, publish\n", 19 | "\n", 20 | "header()" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "662b93db-b40a-4fa5-95fb-f22ddca3d309", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import pytest" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "id": "0eb38aa5-35c6-4079-985a-b91e4859a8de", 36 | "metadata": {}, 37 | "source": [ 38 | "The following will only throw an error on CI, because CI does not mimic the Jupyter Lab environment." 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "e6943826-f6ac-414a-b3b2-e4f8af86823b", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "with pytest.raises(RuntimeError):\n", 49 | " publish(pretend_no_test_env=True)" 50 | ] 51 | } 52 | ], 53 | "metadata": { 54 | "kernelspec": { 55 | "display_name": "Python 3.9.12 ('base1')", 56 | "language": "python", 57 | "name": "python3" 58 | }, 59 | "language_info": { 60 | "codemirror_mode": { 61 | "name": "ipython", 62 | "version": 3 63 | }, 64 | "file_extension": ".py", 65 | "mimetype": "text/x-python", 66 | "name": "python", 67 | "nbconvert_exporter": "python", 68 | "pygments_lexer": "ipython3", 69 | "version": "3.9.12" 70 | }, 71 | "nbproject": { 72 | "id": "mNFd52YOodtk", 73 | "pypackage": null, 74 | "time_init": "2022-07-11T12:16:53.444875+00:00", 75 | "version": "0" 76 | }, 77 | "vscode": { 78 | "interpreter": { 79 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 80 | } 81 | } 82 | }, 83 | "nbformat": 4, 84 | "nbformat_minor": 5 85 | } 86 | -------------------------------------------------------------------------------- /docs/faq/publish-without-title.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "37b372a5-fae0-4177-986f-0adeea86158f", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebooks shows how a warning and an error is raised when there is no title in the first cell." 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "d55181fa-9114-473b-a38d-27349d267f90", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from nbproject import header, meta, publish\n", 19 | "import pytest\n", 20 | "\n", 21 | "header()" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "e5d5dd20", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "meta.live.title # returns None" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "a9665051-9d08-4f52-aa8e-7ffe1e5514e9", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "publish() == \"no-title\"" 42 | ] 43 | } 44 | ], 45 | "metadata": { 46 | "kernelspec": { 47 | "display_name": "Python 3.9.12 ('base1')", 48 | "language": "python", 49 | "name": "python3" 50 | }, 51 | "language_info": { 52 | "codemirror_mode": { 53 | "name": "ipython", 54 | "version": 3 55 | }, 56 | "file_extension": ".py", 57 | "mimetype": "text/x-python", 58 | "name": "python", 59 | "nbconvert_exporter": "python", 60 | "pygments_lexer": "ipython3", 61 | "version": "3.9.12" 62 | }, 63 | "nbproject": { 64 | "id": "aK1IQUYIQsMs", 65 | "pypackage": { 66 | "nbproject": "0.0.7+2.g8521e30" 67 | }, 68 | "time_init": "2022-06-08T14:42:31.551211+00:00", 69 | "version": "0" 70 | }, 71 | "vscode": { 72 | "interpreter": { 73 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 74 | } 75 | } 76 | }, 77 | "nbformat": 4, 78 | "nbformat_minor": 5 79 | } 80 | -------------------------------------------------------------------------------- /docs/faq/publish-wrapper.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1412ea59-223c-41a9-a0f9-81f34240f794", 6 | "metadata": {}, 7 | "source": [ 8 | "# Write a wrapper for `publish()`" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "f2916b1c-6904-415c-a86d-1360af969119", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from nbproject import header, publish\n", 19 | "\n", 20 | "header()" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "5521ec86-e9e0-464f-a167-78ddf74e6efb", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "def my_publish(**kwargs):\n", 31 | " return publish(calling_statement=\"my_publish(\", **kwargs)" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "58869e8c-31b5-4b07-af74-22d833bdd016", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "my_publish(i_confirm_i_saved=True, consecutiveness=False)" 42 | ] 43 | } 44 | ], 45 | "metadata": { 46 | "kernelspec": { 47 | "display_name": "Python 3.9.12 ('base1')", 48 | "language": "python", 49 | "name": "python3" 50 | }, 51 | "language_info": { 52 | "codemirror_mode": { 53 | "name": "ipython", 54 | "version": 3 55 | }, 56 | "file_extension": ".py", 57 | "mimetype": "text/x-python", 58 | "name": "python", 59 | "nbconvert_exporter": "python", 60 | "pygments_lexer": "ipython3", 61 | "version": "3.9.12" 62 | }, 63 | "nbproject": { 64 | "id": "z1EEDTe3gt3R", 65 | "pypackage": { 66 | "nbproject": "0.1.6" 67 | }, 68 | "time_init": "2022-07-12T14:56:54.643320+00:00", 69 | "version": "0" 70 | }, 71 | "vscode": { 72 | "interpreter": { 73 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 74 | } 75 | } 76 | }, 77 | "nbformat": 4, 78 | "nbformat_minor": 5 79 | } 80 | -------------------------------------------------------------------------------- /docs/faq/set-env-via-environment-var.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "b37af009", 6 | "metadata": {}, 7 | "source": [ 8 | "# Set nbproject env using the environment variable" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "8b0b130b", 14 | "metadata": {}, 15 | "source": [ 16 | "By setting `\"NBPRJ_TEST_NBENV\"` it is possible to overwrite `env` identified by `nbproject`." 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "id": "950cfe0c", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "import os\n", 27 | "\n", 28 | "os.environ[\"NBPRJ_TEST_NBENV\"] = \"set_test\"" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "id": "8f72e170", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "from nbproject.dev import notebook_path" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "a2149c9d", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "filename, env = notebook_path(return_env=True)" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "id": "de5ae800", 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "assert env == \"set_test\"" 59 | ] 60 | } 61 | ], 62 | "metadata": { 63 | "kernelspec": { 64 | "display_name": "Python 3 (ipykernel)", 65 | "language": "python", 66 | "name": "python3" 67 | }, 68 | "language_info": { 69 | "codemirror_mode": { 70 | "name": "ipython", 71 | "version": 3 72 | }, 73 | "file_extension": ".py", 74 | "mimetype": "text/x-python", 75 | "name": "python", 76 | "nbconvert_exporter": "python", 77 | "pygments_lexer": "ipython3", 78 | "version": "3.9.12" 79 | } 80 | }, 81 | "nbformat": 4, 82 | "nbformat_minor": 5 83 | } 84 | -------------------------------------------------------------------------------- /docs/faq/setup.md: -------------------------------------------------------------------------------- 1 | # Setup guide 2 | 3 | ## Local installation 4 | 5 | Jupyter Lab: 6 | 7 | - any pip or conda installation 8 | - brew installation may give a problems 9 | 10 | Jupyter Notebook: 11 | 12 | - any pip or conda installation 13 | 14 | ## Saturn Cloud 15 | 16 | Runs [out-of-the-box](https://github.com/laminlabs/run-lamin-on-saturn). 17 | 18 | ## Google Cloud Vertex AI 19 | 20 | For both managed and user-managed notebooks: 21 | 22 | ``` 23 | pip install ipylab==0.5.2 --user 24 | pip install nbproject 25 | ``` 26 | 27 | After the installation, close the Vertex Jupyter Lab interface page and open it again. 28 | 29 | Note: ipylab 0.6.0 is not yet compatible with current Vertex AI but likely will be in the future (2022-08-29.) 30 | 31 | Related issue: [#214](https://github.com/laminlabs/nbproject/issues/214). 32 | -------------------------------------------------------------------------------- /docs/faq/title-not-at-top.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "37b372a5-fae0-4177-986f-0adeea86158f", 6 | "metadata": {}, 7 | "source": [ 8 | "Some other content here." 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "411d20ca-f637-4d3f-9047-1b5782b1d7ea", 14 | "metadata": {}, 15 | "source": [ 16 | "# Title not at top" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "id": "d55181fa-9114-473b-a38d-27349d267f90", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "from nbproject import header, meta, publish\n", 27 | "import pytest\n", 28 | "\n", 29 | "header()" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "id": "e5d5dd20", 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "assert meta.live.title == \"Title not at top\"" 40 | ] 41 | } 42 | ], 43 | "metadata": { 44 | "kernelspec": { 45 | "display_name": "Python 3.9.12 ('base1')", 46 | "language": "python", 47 | "name": "python3" 48 | }, 49 | "language_info": { 50 | "codemirror_mode": { 51 | "name": "ipython", 52 | "version": 3 53 | }, 54 | "file_extension": ".py", 55 | "mimetype": "text/x-python", 56 | "name": "python", 57 | "nbconvert_exporter": "python", 58 | "pygments_lexer": "ipython3", 59 | "version": "3.9.16" 60 | }, 61 | "nbproject": { 62 | "id": "aK1IQUYIQsMs", 63 | "pypackage": { 64 | "nbproject": "0.0.7+2.g8521e30" 65 | }, 66 | "time_init": "2022-06-08T14:42:31.551211+00:00", 67 | "version": "0" 68 | }, 69 | "vscode": { 70 | "interpreter": { 71 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 72 | } 73 | } 74 | }, 75 | "nbformat": 4, 76 | "nbformat_minor": 5 77 | } 78 | -------------------------------------------------------------------------------- /docs/faq/trigger-exit-upon-init.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Trigger SystemExit upon init in non-interactive editor" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import pytest\n", 17 | "from nbproject import header" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# header() this should be ignored" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "with pytest.raises(SystemExit):\n", 36 | " header(\n", 37 | " parent=[\"z14KWQKD4bwE\", \"r4Dkcgt2HIfh\"], pypackage=\"anndata\"\n", 38 | " ) # header() triggers exit init" 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3 (ipykernel)", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 3 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython3", 58 | "version": "3.9.12" 59 | }, 60 | "vscode": { 61 | "interpreter": { 62 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 63 | } 64 | } 65 | }, 66 | "nbformat": 4, 67 | "nbformat_minor": 4 68 | } 69 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | Manage notebooks interactively: 4 | 5 | - {doc}`quickstart` 6 | - {doc}`guide/received` 7 | - {doc}`guide/basic-metadata` 8 | - {doc}`guide/update-metadata` 9 | 10 | Build on the metadata API: 11 | 12 | - {doc}`guide/meta` 13 | 14 | ```{toctree} 15 | :maxdepth: 1 16 | :hidden: 17 | 18 | quickstart 19 | guide/received 20 | guide/basic-metadata 21 | guide/update-metadata 22 | guide/meta 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/guide/basic-metadata.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "cc6c2fe6-6291-4a91-b326-b1ec6cb8d703", 6 | "metadata": { 7 | "tags": [] 8 | }, 9 | "source": [ 10 | "# Basic metadata" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "705ac1d5-cbd9-421f-841e-d91e15faa962", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from nbproject import header\n", 21 | "\n", 22 | "header()" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "id": "b113b603-5d6d-4096-90e5-bca19d64a5ce", 28 | "metadata": { 29 | "tags": [] 30 | }, 31 | "source": [ 32 | "## `id`\n", 33 | "\n", 34 | "The full 12-character ID makes your notebook unique. It keeps the chance that someone else has a notebook with the same ID below 1/1M even if you compare with 100M other notebooks. This is because there are `>1e21` such 12-character IDs.\n", 35 | "\n", 36 | "In practical settings, remembering the first 4 digits is good enough. They keep the collision probability below 1/1k for comparisons with up to 200 notebooks.\n", 37 | "\n", 38 | "## `version`\n", 39 | "\n", 40 | "This is set to `\"0\"` upon initialization and either automatically set or set to any desired version upon `publish()`.\n", 41 | "\n", 42 | "## `time_init`\n", 43 | "\n", 44 | "This is the time stamp generated upon initialization of nbproject. If you use nbproject on every new notebook, it coincides with the time of notebook creation.\n", 45 | "\n", 46 | "The time is displayed in UTC instead of a time-zone dependent local time.\n", 47 | "\n", 48 | "## `time_run`\n", 49 | "\n", 50 | "This is the time at which you're running the current notebook.\n", 51 | "\n", 52 | "## `pypackage`\n", 53 | "\n", 54 | "Automatically inferred imported packages and their versions. Can be supplemented with manually set packages.\n", 55 | "\n", 56 | "## `parent`\n", 57 | "\n", 58 | "Another pre-defined field in `nbproject` is called `parent`. You need to set it entirely manually right now, but you can use it to denote upstream dependencies of other notebooks you used in your workflow." 59 | ] 60 | } 61 | ], 62 | "metadata": { 63 | "kernelspec": { 64 | "display_name": "Python 3 (ipykernel)", 65 | "language": "python", 66 | "name": "python3" 67 | }, 68 | "language_info": { 69 | "codemirror_mode": { 70 | "name": "ipython", 71 | "version": 3 72 | }, 73 | "file_extension": ".py", 74 | "mimetype": "text/x-python", 75 | "name": "python", 76 | "nbconvert_exporter": "python", 77 | "pygments_lexer": "ipython3", 78 | "version": "3.9.12" 79 | }, 80 | "nbproject": { 81 | "id": "z14KWQKD4bwE", 82 | "time_init": "2022-04-18T22:07:44.214819+00:00", 83 | "version": "0" 84 | }, 85 | "vscode": { 86 | "interpreter": { 87 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 88 | } 89 | } 90 | }, 91 | "nbformat": 4, 92 | "nbformat_minor": 5 93 | } 94 | -------------------------------------------------------------------------------- /docs/guide/meta.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1b4d3825-6110-440b-a9c0-99cbb56f6ab6", 6 | "metadata": {}, 7 | "source": [ 8 | "# The metadata API" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "cba70490-5b5d-4e11-88a6-5bb338d32810", 14 | "metadata": {}, 15 | "source": [ 16 | "We already learned how to display metadata to provide humans with context (e.g., {doc}`basic-metadata`).\n", 17 | "\n", 18 | "If we want to build functionality on top of notebook metadata, we can access metadata through the API." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "a920ea06-3b40-4900-a83e-9aaf8632c098", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "from nbproject import header, meta\n", 29 | "import pandas as pd\n", 30 | "\n", 31 | "header()" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "id": "d8052d5b", 37 | "metadata": {}, 38 | "source": [ 39 | "If you'd like a simple dictionary and intialization state, call:" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "id": "5047184e", 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "header(metadata_only=True)" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "id": "f38546af-5976-482e-97be-ab4cc025b420", 55 | "metadata": {}, 56 | "source": [ 57 | "For instance, we can retrieve the stored notebook ID as follows." 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "c57e6a11-7d4f-4e39-9972-d6c9e4bd3412", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "assert not meta.live.consecutive_cells" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "id": "2fa7f01c-5187-43cc-aaab-67ebebf8c043", 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "meta.store.id" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": null, 83 | "id": "2361d6ac-7468-4bb3-9491-e7724f58c653", 84 | "metadata": { 85 | "tags": [ 86 | "hide-cell" 87 | ] 88 | }, 89 | "outputs": [], 90 | "source": [ 91 | "assert meta.store.id == \"k6Bj4FXM9oPm\"\n", 92 | "assert hasattr(meta.store, \"time_init\")" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "id": "092b9b36", 99 | "metadata": {}, 100 | "outputs": [], 101 | "source": [ 102 | "meta.store.pypackage" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "id": "5b314ad2", 108 | "metadata": {}, 109 | "source": [ 110 | "Manually add pypackages to `meta.store.pypackage`." 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "id": "c89ede67", 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "meta.store.add_pypackages([\"pytest\"])" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "id": "1e48fe0a", 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "meta.store.pypackage" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "id": "774584db-2ecc-40ba-9445-05fad86018cd", 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "meta.live.title" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "id": "46074a8f-e2c3-4420-a5a9-131c4afbecbc", 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "# assert meta.live.consecutive_cells # it'd be nice to test this here, but that would require flushing the ci buffer before this cell" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "id": "32c40a13-7ca5-4f92-a874-5a925488b582", 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "meta.live.time_run" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": null, 166 | "id": "e43f7cf2-2ec0-4c5b-9cf9-b57c7d703425", 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [ 170 | "meta.live.time_passed" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "id": "9e230c96-c707-4247-a53f-8a5850f380b2", 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "meta.live.pypackage" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": null, 186 | "id": "e4a9cdb0-c1d8-4541-b04c-3d34d08faac7", 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "meta.live" 191 | ] 192 | } 193 | ], 194 | "metadata": { 195 | "kernelspec": { 196 | "display_name": "Python 3 (ipykernel)", 197 | "language": "python", 198 | "name": "python3" 199 | }, 200 | "language_info": { 201 | "codemirror_mode": { 202 | "name": "ipython", 203 | "version": 3 204 | }, 205 | "file_extension": ".py", 206 | "mimetype": "text/x-python", 207 | "name": "python", 208 | "nbconvert_exporter": "python", 209 | "pygments_lexer": "ipython3", 210 | "version": "3.9.12" 211 | }, 212 | "nbproject": { 213 | "id": "k6Bj4FXM9oPm", 214 | "time_init": "2022-06-29T14:38:42.171771+00:00", 215 | "version": "0" 216 | }, 217 | "vscode": { 218 | "interpreter": { 219 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 220 | } 221 | } 222 | }, 223 | "nbformat": 4, 224 | "nbformat_minor": 5 225 | } 226 | -------------------------------------------------------------------------------- /docs/guide/received.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Continue someone else's work" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "We're still in the same notebook (`3m2Q`), but let's imagine we received it from someone else and we're running it in a different environment." 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "from nbproject import header, publish, meta\n", 24 | "\n", 25 | "header()" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "The header now makes package version mismatches evident. " 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "import pandas" 42 | ] 43 | } 44 | ], 45 | "metadata": { 46 | "kernelspec": { 47 | "display_name": "Python 3 (ipykernel)", 48 | "language": "python", 49 | "name": "python3" 50 | }, 51 | "language_info": { 52 | "codemirror_mode": { 53 | "name": "ipython", 54 | "version": 3 55 | }, 56 | "file_extension": ".py", 57 | "mimetype": "text/x-python", 58 | "name": "python", 59 | "nbconvert_exporter": "python", 60 | "pygments_lexer": "ipython3", 61 | "version": "3.9.12" 62 | }, 63 | "nbproject": { 64 | "id": "3m2Q6UBuwgSH", 65 | "parent": [ 66 | "z14KWQKD4bwE", 67 | "jhvyoIrxoeSz" 68 | ], 69 | "pypackage": { 70 | "nbproject": "0.1.6", 71 | "pandas": "1.4.1" 72 | }, 73 | "time_init": "2022-07-18T12:32:21.625107+00:00", 74 | "version": "0" 75 | }, 76 | "vscode": { 77 | "interpreter": { 78 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 79 | } 80 | } 81 | }, 82 | "nbformat": 4, 83 | "nbformat_minor": 4 84 | } 85 | -------------------------------------------------------------------------------- /docs/guide/update-metadata.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "4c5d9cf8", 6 | "metadata": { 7 | "tags": [] 8 | }, 9 | "source": [ 10 | "# Update metadata" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "c0aad033-1af5-436d-a8f7-7f5aedba41a8", 16 | "metadata": {}, 17 | "source": [ 18 | "You can add additional packages to be tracked right at initialization:" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "0f734180-2bf1-4341-b068-7ab84764bf57", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "from nbproject import header, meta\n", 29 | "import pandas\n", 30 | "\n", 31 | "header(\n", 32 | " parent=[\n", 33 | " \"z14KWQKD4bwE\",\n", 34 | " \"jhvyoIrxoeSz\",\n", 35 | " ],\n", 36 | " pypackage=\"nbformat\", # Indirect dependencies to be tracked. Direct dependencies are automatically tracked.\n", 37 | ")" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "bd7d72a2-8fcd-4414-b957-d233664d0fe1", 43 | "metadata": {}, 44 | "source": [ 45 | "Use the following convenience function to update packages." 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "id": "ff350260", 52 | "metadata": { 53 | "tags": [] 54 | }, 55 | "outputs": [], 56 | "source": [ 57 | "meta.store.add_pypackages([\"numpy\", \"pytest\"]).write()" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "id": "cbea77ad-82a9-4b8f-9081-c9bf3ccda0c3", 63 | "metadata": {}, 64 | "source": [ 65 | "Update `parent` or any custom metadata field via: " 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "id": "7b4166ad-907a-4153-9cb3-5f74a7d0e5d9", 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "meta.store.parent = [\n", 76 | " \"z14KWQKD4bwE\",\n", 77 | " \"jhvyoIrxoeSz\",\n", 78 | " \"3m2Q6UBuwgSH\",\n", 79 | "]" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "id": "2eb0ac87-8338-4a4c-bab1-d84e3c6748be", 85 | "metadata": {}, 86 | "source": [ 87 | "Don't forget to write the changes from the store to the file!" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "id": "ea6238da", 94 | "metadata": { 95 | "tags": [] 96 | }, 97 | "outputs": [], 98 | "source": [ 99 | "meta.store.write()" 100 | ] 101 | } 102 | ], 103 | "metadata": { 104 | "kernelspec": { 105 | "display_name": "Python 3 (ipykernel)", 106 | "language": "python", 107 | "name": "python3" 108 | }, 109 | "language_info": { 110 | "codemirror_mode": { 111 | "name": "ipython", 112 | "version": 3 113 | }, 114 | "file_extension": ".py", 115 | "mimetype": "text/x-python", 116 | "name": "python", 117 | "nbconvert_exporter": "python", 118 | "pygments_lexer": "ipython3", 119 | "version": "3.9.12" 120 | }, 121 | "nbproject": { 122 | "id": "CfcLn8WBSlKt", 123 | "parent": [ 124 | "z14KWQKD4bwE", 125 | "jhvyoIrxoeSz" 126 | ], 127 | "pypackage": { 128 | "nbformat": "5.4.0", 129 | "nbproject": "0.1.6", 130 | "pandas": "1.4.2" 131 | }, 132 | "time_init": "2022-07-18T13:04:49.875497+00:00", 133 | "version": "0" 134 | }, 135 | "vscode": { 136 | "interpreter": { 137 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 138 | } 139 | } 140 | }, 141 | "nbformat": 4, 142 | "nbformat_minor": 5 143 | } 144 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ```{include} ../README.md 2 | :start-line: 0 3 | :end-line: -2 4 | ``` 5 | 6 | ```{toctree} 7 | :maxdepth: 1 8 | :hidden: 9 | 10 | guide 11 | reference 12 | faq 13 | changelog 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/quickstart.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Quickstart" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "![](https://lamin-site-assets.s3.amazonaws.com/.lamindb/NHq29ckKVrrTYsNdG0KjT.gif)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "1. {func}`~nbproject.header` provides relevant context.\n", 22 | "2. {func}`~nbproject.publish` prepares a notebook for sharing it with someone else." 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "from nbproject import header, publish, meta\n", 32 | "\n", 33 | "header(parent=[\"z14KWQKD4bwE\", \"jhvyoIrxoeSz\"]) # Initializes & displays metadata." 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "```{note}\n", 41 | "\n", 42 | "- Passing `parent` to `header()` is optional. You can use it to point viewers to upstream notebooks or to build pipelines.\n", 43 | "- You can also pass `pypackage` to `header()` to track secondary dependencies.\n", 44 | "\n", 45 | "```" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "import pandas # Any imported package is automatically tracked." 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "Once you're happy with the present version of your notebook:" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "publish() # Sets version, checks consecutiveness & title, writes current pypackages with versions." 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "metadata": {}, 76 | "source": [ 77 | "```{note}\n", 78 | "\n", 79 | "If you're a developer and want to build on top of nbproject, the API offers:\n", 80 | "\n", 81 | "- {class}`~nbproject.meta` to access metadata: {doc}`guide/meta`.\n", 82 | "- {mod}`~nbproject.dev` for development utils.\n", 83 | "```" 84 | ] 85 | } 86 | ], 87 | "metadata": { 88 | "kernelspec": { 89 | "display_name": "Python 3 (ipykernel)", 90 | "language": "python", 91 | "name": "python3" 92 | }, 93 | "language_info": { 94 | "codemirror_mode": { 95 | "name": "ipython", 96 | "version": 3 97 | }, 98 | "file_extension": ".py", 99 | "mimetype": "text/x-python", 100 | "name": "python", 101 | "nbconvert_exporter": "python", 102 | "pygments_lexer": "ipython3", 103 | "version": "3.9.12" 104 | }, 105 | "nbproject": { 106 | "id": "3m2Q6UBuwgSH", 107 | "parent": [ 108 | "z14KWQKD4bwE", 109 | "jhvyoIrxoeSz" 110 | ], 111 | "time_init": "2022-07-18T12:32:21.625107+00:00", 112 | "version": "0" 113 | }, 114 | "vscode": { 115 | "interpreter": { 116 | "hash": "2775e555cdc2d728c54aa22130c79afb1fa4da64f22f2fc6dcc2aa346c4e0672" 117 | } 118 | } 119 | }, 120 | "nbformat": 4, 121 | "nbformat_minor": 4 122 | } 123 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | ```{eval-rst} 4 | .. automodule:: nbproject 5 | ``` 6 | -------------------------------------------------------------------------------- /lamin-project.yaml: -------------------------------------------------------------------------------- 1 | project_name: nbproject 2 | description: Manage Jupyter notebooks 3 | project_slug: nbproject 4 | repository_name: nbproject 5 | package_name: nbproject 6 | -------------------------------------------------------------------------------- /nbproject/__init__.py: -------------------------------------------------------------------------------- 1 | """nbproject: Manage Jupyter notebooks. 2 | 3 | .. currentmodule:: nbproject 4 | 5 | Most users will only need these two functions: 6 | 7 | .. autosummary:: 8 | :toctree: 9 | 10 | header 11 | publish 12 | 13 | Use them with default arguments after importing them like this:: 14 | 15 | from nbproject import header, publish 16 | 17 | For more fine-grained access, use: 18 | 19 | .. autosummary:: 20 | :toctree: 21 | 22 | meta 23 | dev 24 | 25 | """ 26 | __version__ = "0.11.1" 27 | 28 | from .dev._jupyter_lab_commands import _init_frontend 29 | 30 | # init jupyter lab frontend immediately on import 31 | # nothing happens if this is not jupyter lab 32 | try: 33 | _init_frontend() 34 | except: # noqa: E722 35 | pass 36 | 37 | from . import dev 38 | from ._header import header 39 | from ._meta import meta 40 | from ._publish import publish 41 | -------------------------------------------------------------------------------- /nbproject/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from ._cli import init, publish, reqs, sync 4 | 5 | 6 | def main(): 7 | parser = argparse.ArgumentParser(prog="nbproject") 8 | subparsers = parser.add_subparsers(help="available commands:", dest="cmd") 9 | 10 | parser_init = subparsers.add_parser("init", help="init the project") # noqa: F841 11 | 12 | parser_sync = subparsers.add_parser( 13 | "sync", help="synchronize the notebooks of the project" 14 | ) 15 | parser_sync.add_argument( 16 | "files_dirs", nargs="+", help="which files and folders to synchronize" 17 | ) 18 | parser_sync.add_argument( 19 | "--deps", 20 | "-d", 21 | action="store_true", 22 | help=( 23 | "parse pypackages from the notebooks and pin versions from the current" 24 | " environment" 25 | ), 26 | ) 27 | parser_sync.add_argument( 28 | "--no-versions", 29 | "-nv", 30 | action="store_true", 31 | help="do not pin the versions from the current environment", 32 | ) 33 | 34 | parser_reqs = subparsers.add_parser("reqs", help="create requirments.txt") 35 | parser_reqs.add_argument( 36 | "files_dirs", nargs="+", help="create requirments.txt for these files" 37 | ) 38 | 39 | parser_publish = subparsers.add_parser("publish", help="pubish the notebooks") 40 | parser_publish.add_argument("files_dirs", nargs="+", help="publish these notebooks") 41 | 42 | args = parser.parse_args() 43 | 44 | if args.cmd == "init": 45 | init() 46 | elif args.cmd == "sync": 47 | sync(args.files_dirs, args.deps, not args.no_versions) 48 | elif args.cmd == "reqs": 49 | reqs(args.files_dirs) 50 | elif args.cmd == "publish": 51 | publish(args.files_dirs) 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /nbproject/_cli.py: -------------------------------------------------------------------------------- 1 | # This file contains the functions to process the cli commands. 2 | from itertools import chain 3 | from pathlib import Path 4 | from typing import Iterator, Union 5 | 6 | import yaml # type: ignore 7 | 8 | from ._logger import logger 9 | from ._schemas import NBRecord, YAMLRecord 10 | from .dev._notebook import read_notebook, write_notebook 11 | from .dev._pypackage import infer_pypackages, resolve_versions 12 | 13 | 14 | def find_upwards(cwd: Path, filename: str): 15 | if cwd == Path(cwd.anchor): 16 | return None 17 | 18 | fullpath = cwd / filename 19 | 20 | return fullpath if fullpath.exists() else find_upwards(cwd.parent, filename) 21 | 22 | 23 | def notebooks_from_files_dirs(files_dirs: Iterator[Union[str, Path]]): 24 | nbs = [] 25 | 26 | for file_dir in files_dirs: 27 | file_dir = Path(file_dir) 28 | if file_dir.is_dir(): 29 | nbs.append(file_dir.glob("**/*.ipynb")) 30 | else: 31 | if file_dir.suffix == ".ipynb": 32 | nbs.append([file_dir]) # type: ignore 33 | else: 34 | logger.info(f"The file {file_dir} is not a notebook, ignoring.") 35 | 36 | return chain(*nbs) 37 | 38 | 39 | def init(): 40 | cwd = Path.cwd() 41 | 42 | yaml_filled = find_upwards(cwd, "nbproject_metadata.yml") 43 | if yaml_filled is not None: 44 | logger.info("You are already in the nbproject (sub)folder.") 45 | logger.info(f"Yaml of the project is: {yaml_filled.as_posix()}.") 46 | return 47 | 48 | nbs = cwd.glob("**/*.ipynb") 49 | 50 | init_yaml = {} 51 | 52 | for nb_path in nbs: 53 | if ".ipynb_checkpoints/" in nb_path.as_posix(): 54 | continue 55 | 56 | nb_path = nb_path.relative_to(cwd) 57 | 58 | nb = read_notebook(nb_path) 59 | 60 | nbproj_record = NBRecord(nb) 61 | nbproj_record.write(nb_path, overwrite=False) 62 | 63 | yaml_record = YAMLRecord(nb_path, nbproj_record, init_yaml) 64 | yaml_record.put_yaml() 65 | 66 | new_file = "nbproject_metadata.yml" 67 | with open(new_file, "w") as stream: 68 | yaml.dump(init_yaml, stream, sort_keys=False) 69 | logger.info(f"Created {cwd / new_file}.") 70 | 71 | 72 | def sync( 73 | files_dirs: Iterator[str], parse_deps: bool = False, pin_versions: bool = False 74 | ): 75 | cwd = Path.cwd() 76 | yaml_file = find_upwards(cwd, "nbproject_metadata.yml") 77 | 78 | if yaml_file is None: 79 | logger.info("You are not inside an nbproject folder, use init.") 80 | return 81 | else: 82 | logger.info(f"Yaml of the project is: {yaml_file.as_posix()}.") 83 | 84 | with open(yaml_file) as stream: 85 | yaml_proj = yaml.load(stream, Loader=yaml.FullLoader) 86 | 87 | nbs = notebooks_from_files_dirs(files_dirs) 88 | n_nbs = 0 89 | for nb_path in nbs: 90 | if ".ipynb_checkpoints/" in nb_path.as_posix(): 91 | continue 92 | n_nbs += 1 93 | 94 | nb = read_notebook(nb_path) 95 | 96 | nbproj_record = NBRecord(nb) 97 | yaml_record = YAMLRecord(nb_path, nbproj_record, yaml_proj) 98 | 99 | if parse_deps: 100 | deps = infer_pypackages(nb, pin_versions=pin_versions) 101 | yaml_record.pypackage = deps # type: ignore 102 | 103 | yaml_record.put_metadata() 104 | yaml_record.put_yaml() 105 | 106 | nbproj_record.write(nb_path, overwrite=False) 107 | 108 | with open(yaml_file, "w") as stream: 109 | yaml.dump(yaml_proj, stream, sort_keys=False) 110 | logger.info(f"Synced {n_nbs} notebooks.") 111 | 112 | 113 | def reqs(files_dirs: Iterator[str]): 114 | # todo: check different versions and do some resolution for conflicting versions 115 | gather_deps = [] 116 | 117 | nbs = notebooks_from_files_dirs(files_dirs) 118 | for nb_path in nbs: 119 | if ".ipynb_checkpoints/" in nb_path.as_posix(): 120 | continue 121 | 122 | nb = read_notebook(nb_path) 123 | 124 | if "nbproject" not in nb.metadata: 125 | logger.info( 126 | "Uninitialized or unsynced notebooks, use > nbproject init or >" 127 | " nbproject sync ." 128 | ) 129 | return 130 | nbproj_metadata = nb.metadata["nbproject"] 131 | if "pypackage" in nbproj_metadata: 132 | gather_deps.append(nbproj_metadata["pypackage"]) 133 | 134 | deps = resolve_versions(gather_deps) 135 | deps = [pkg + f"=={ver}" if ver != "" else pkg for pkg, ver in deps.items()] 136 | 137 | requirments = "\n".join(deps) 138 | with open("requirments.txt", "w") as stream: 139 | stream.write(requirments) 140 | 141 | logger.info("Created `requirements.txt`.") 142 | 143 | 144 | def publish(files_dirs: Iterator[str]): 145 | nbs = notebooks_from_files_dirs(files_dirs) 146 | n_nbs = 0 147 | for nb_path in nbs: 148 | if ".ipynb_checkpoints/" in nb_path.as_posix(): 149 | continue 150 | 151 | nb = read_notebook(nb_path) 152 | 153 | if "nbproject" in nb.metadata: 154 | nbproject_meta = nb.metadata["nbproject"] 155 | add_pkgs = None 156 | if "pypackage" in nbproject_meta: 157 | add_pkgs = nbproject_meta["pypackage"].keys() 158 | nbproject_meta["pypackage"] = infer_pypackages( 159 | nb, add_pkgs, pin_versions=True 160 | ) 161 | 162 | version = "1" 163 | if "version" in nbproject_meta: 164 | version = nbproject_meta["version"] 165 | nbproject_meta["version"] = version 166 | 167 | write_notebook(nb, nb_path) 168 | 169 | n_nbs += 1 170 | 171 | logger.info(f"Pubished {n_nbs} notebooks.") 172 | -------------------------------------------------------------------------------- /nbproject/_header.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime, timezone 3 | from typing import List, Mapping, Optional, Tuple, Union 4 | 5 | from ._logger import logger 6 | from .dev._frontend_commands import _reload_notebook, _save_notebook 7 | from .dev._initialize import initialize_metadata 8 | from .dev._jupyter_communicate import notebook_path 9 | from .dev._jupyter_lab_commands import _ipylab_is_installed 10 | from .dev._metadata_display import display_html, table_metadata 11 | from .dev._notebook import Notebook, read_notebook, write_notebook 12 | 13 | _filepath = None 14 | _env = None 15 | _time_run = None 16 | 17 | 18 | msg_init_complete = ( 19 | "Init complete. Hit save & reload from disk, i.e, *discard* editor content. If you" 20 | " do not want to lose editor changes, hit save *before* running `header()`." 21 | " Consider using Jupyter Lab with ipylab installed for a seamless interactive experience." 22 | ) 23 | 24 | msg_inconsistent_parent = ( 25 | "Argument parent is inconsistent with store.\nPlease update" 26 | " metadata, e.g.: meta.store.parent = parent; meta.store.write()" 27 | ) 28 | 29 | 30 | def msg_inconsistent_pypackage(pypackage): 31 | return ( 32 | "Argument pypackage is inconsistent with metadata store.\nPlease update" 33 | f' metadata: meta.store.add_pypackages("{pypackage}").write()' 34 | ) 35 | 36 | 37 | def _output_table(notebook: Notebook, table: str): 38 | out = { 39 | "data": { 40 | "text/html": [table], 41 | "text/plain": [""], 42 | }, 43 | "metadata": {}, 44 | "output_type": "display_data", 45 | } 46 | 47 | header_re = re.compile(r"^[^#]*header\(", flags=re.MULTILINE) 48 | ccount = 0 49 | for cell in notebook.cells: 50 | if cell["cell_type"] != "code": 51 | continue 52 | elif cell["execution_count"] is not None: 53 | ccount = cell["execution_count"] 54 | 55 | if header_re.match("".join(cell["source"])) is not None: 56 | cell["outputs"] = [out] 57 | cell["execution_count"] = ccount + 1 58 | # update only once 59 | break 60 | 61 | 62 | def header( 63 | *, 64 | parent: Union[str, List[str], None] = None, 65 | pypackage: Union[str, List[str], None] = None, 66 | filepath: Union[str, None] = None, 67 | env: Union[str, None] = None, 68 | metadata_only: bool = False, 69 | ) -> Optional[Tuple[Mapping, bool, Notebook]]: 70 | """Display metadata and start tracking dependencies. 71 | 72 | If the notebook has no nbproject metadata, initializes & writes metadata to disk. 73 | 74 | Args: 75 | parent: One or more nbproject ids of direct ancestors in a notebook pipeline. 76 | pypackage: One or more python packages to track. 77 | filepath: Filepath of notebook. Only needed if automatic inference fails. 78 | env: Editor environment. Only needed if automatic inference fails. 79 | Pass `'lab'` for jupyter lab and `'notebook'` for jupyter notebook, 80 | this can help to identify the correct mechanism for interactivity 81 | when automatic inference fails. 82 | metadata_only: Whether or not to return only metadata 83 | without writing or displaying anything. 84 | """ 85 | filepath_env = filepath, env 86 | 87 | if filepath is None: 88 | filepath_env = notebook_path(return_env=True) 89 | if filepath_env is None: 90 | logger.info( 91 | "Can't infer the name of the current notebook, " 92 | "you are probably not inside a Jupyter notebook. " 93 | "Please call `header(filepath='your-file.ipynb')`." 94 | ) 95 | return None 96 | filepath = filepath_env[0] 97 | 98 | if env is None: 99 | env = filepath_env[1] 100 | # The following occurs when passing filepath manually 101 | # We assume Jupyter Lab as an environment for now 102 | if env is None: 103 | env = "lab" 104 | 105 | try: 106 | nb = read_notebook(filepath) # type: ignore 107 | except FileNotFoundError: 108 | raise RuntimeError( 109 | "Try passing the filepath manually to nbproject.Header()." 110 | ) from None 111 | 112 | # make time_run available through API 113 | time_run = datetime.now(timezone.utc) 114 | global _time_run, _filepath, _env 115 | _time_run, _filepath, _env = time_run, filepath, env 116 | 117 | interactive_envs = ["notebook"] 118 | if _ipylab_is_installed(): 119 | interactive_envs.append("lab") 120 | 121 | # initialize 122 | if "nbproject" not in nb.metadata: 123 | if env in interactive_envs: 124 | _save_notebook(env) 125 | nb = read_notebook(filepath) # type: ignore 126 | 127 | metadata = initialize_metadata(nb, parent=parent, pypackage=pypackage).dict() 128 | 129 | if metadata_only: 130 | # True here means that the metdata has been initialized now 131 | return metadata, True, nb 132 | else: 133 | nb.metadata["nbproject"] = metadata 134 | _output_table(nb, table_metadata(metadata, nb, time_run)) 135 | write_notebook(nb, filepath) # type: ignore 136 | 137 | if env in interactive_envs: 138 | _reload_notebook(env) 139 | else: 140 | raise SystemExit(msg_init_complete) 141 | 142 | # read from ipynb metadata and add on-the-fly computed metadata 143 | else: 144 | metadata = nb.metadata["nbproject"] 145 | if not metadata_only: 146 | table = table_metadata(metadata, nb, time_run) 147 | display_html(table) 148 | 149 | # check whether updates to init are needed 150 | if parent is not None: 151 | if "parent" not in metadata or metadata["parent"] != parent: 152 | logger.info(msg_inconsistent_parent) 153 | if pypackage is not None: 154 | pypackage = [pypackage] if isinstance(pypackage, str) else pypackage 155 | is_empty = "pypackage" not in metadata or metadata["pypackage"] is None 156 | for pkg in pypackage: 157 | if is_empty or pkg not in metadata["pypackage"]: 158 | logger.info(msg_inconsistent_pypackage(pkg)) 159 | 160 | if metadata_only: 161 | # False here means that the notebook has the metadata already 162 | return metadata, False, nb 163 | 164 | return None 165 | -------------------------------------------------------------------------------- /nbproject/_is_run_from_ipython.py: -------------------------------------------------------------------------------- 1 | # From https://github.com/scverse/scanpy/blob/d7e13025b931ad4afd03b4344ef5ff4a46f78b2b/scanpy/_settings.py 2 | # Under BSD 3-Clause License 3 | # Copyright (c) 2017 F. Alexander Wolf, P. Angerer, Theis Lab 4 | import builtins 5 | 6 | is_run_from_ipython = getattr(builtins, "__IPYTHON__", False) 7 | -------------------------------------------------------------------------------- /nbproject/_logger.py: -------------------------------------------------------------------------------- 1 | from lamin_utils import colors, logger 2 | -------------------------------------------------------------------------------- /nbproject/_meta.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | 4 | from .dev._jupyter_communicate import notebook_path 5 | from .dev._meta_live import MetaLive 6 | from .dev._meta_store import MetaContainer, MetaStore 7 | from .dev._notebook import read_notebook 8 | 9 | 10 | # https://stackoverflow.com/questions/128573/using-property-on-classmethods/64738850#64738850 11 | class classproperty: 12 | def __init__(self, fget): 13 | self.fget = fget 14 | 15 | def __get__(self, owner_self, owner_cls): 16 | return self.fget(owner_cls) 17 | 18 | 19 | # https://stackoverflow.com/questions/8955754/can-i-define-a-repr-for-a-class-rather-than-an-instance 20 | class MetaRepr(type): 21 | def __repr__(cls): 22 | return ( 23 | "Metadata object with .live and .store metadata fields:\n" 24 | f" .store: {cls.store}\n" 25 | f" .live: {cls.live}" 26 | ) 27 | 28 | 29 | class meta(metaclass=MetaRepr): 30 | """Access `meta.store` and `meta.live`. 31 | 32 | - `meta.store` - nbproject metadata of the ipynb file, see 33 | :class:`~nbproject.dev.MetaStore` 34 | - `meta.live` - execution info and properties derived from notebook content, 35 | see :class:`~nbproject.dev.MetaLive` 36 | 37 | `meta` is a static class and behaves like a module (no need to initialize it). 38 | """ 39 | 40 | _filepath: Union[str, Path, None] = None 41 | _env = None 42 | _time_run = None 43 | 44 | _store: Union[MetaStore, None] = None 45 | _live: Union[MetaLive, None] = None 46 | 47 | @classmethod 48 | def _init_meta(cls): 49 | from ._header import _env, _filepath, _time_run 50 | 51 | env = _env 52 | filepath = _filepath 53 | filepath_env = _filepath, _env 54 | 55 | if filepath is None: 56 | filepath_env = notebook_path(return_env=True) 57 | if filepath_env is None: 58 | filepath_env = None, None 59 | filepath = filepath_env[0] 60 | 61 | if env is None: 62 | env = filepath_env[1] 63 | 64 | cls._filepath = filepath 65 | cls._env = env 66 | cls._time_run = _time_run 67 | 68 | if cls._filepath is not None: 69 | nb_meta = read_notebook(cls._filepath).metadata 70 | else: 71 | nb_meta = None 72 | 73 | if nb_meta is not None and "nbproject" in nb_meta: 74 | meta_container = MetaContainer(**nb_meta["nbproject"]) 75 | else: 76 | empty = "not initialized" 77 | meta_container = MetaContainer(id=empty, time_init=empty, version=empty) 78 | 79 | cls._store = MetaStore(meta_container, cls._filepath, cls._env) 80 | cls._live = MetaLive(cls._filepath, cls._time_run, cls._env) 81 | 82 | @classproperty 83 | def store(cls) -> MetaStore: 84 | """Metadata stored in the notebook.""" 85 | if cls._store is None: 86 | cls._init_meta() 87 | return cls._store # type: ignore 88 | 89 | @classproperty 90 | def live(cls) -> MetaLive: 91 | """Contains execution info and properties of the notebook content.""" 92 | if cls._live is None: 93 | cls._init_meta() 94 | return cls._live # type: ignore 95 | 96 | @classproperty 97 | def env(cls): 98 | """Contains info about execution environment.""" 99 | if cls._env is None: 100 | cls._init_meta() 101 | return cls._env 102 | -------------------------------------------------------------------------------- /nbproject/_publish.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from ._logger import colors, logger 4 | from ._meta import meta 5 | from .dev._check_last_cell import check_last_cell 6 | from .dev._consecutiveness import check_consecutiveness 7 | from .dev._frontend_commands import _save_notebook 8 | from .dev._jupyter_lab_commands import _ipylab_is_installed 9 | from .dev._notebook import read_notebook 10 | from .dev._set_version import set_version 11 | 12 | 13 | def run_checks_for_publish( 14 | *, calling_statement: str, i_confirm_i_saved: bool = False, **kwargs 15 | ): 16 | """Runs all checks for publishing.""" 17 | if meta.env == "notebook" or (meta.env == "lab" and _ipylab_is_installed()): 18 | _save_notebook(meta.env) 19 | else: 20 | pretend_no_test_env = ( 21 | kwargs["pretend_no_test_env"] if "pretend_no_test_env" in kwargs else False 22 | ) 23 | if ( 24 | meta.env == "test" and not pretend_no_test_env 25 | ): # do not raise error in test environment 26 | pass 27 | elif not i_confirm_i_saved: 28 | raise RuntimeError( 29 | "Make sure you save the notebook in your editor before publishing!\n" 30 | "You can avoid the need for manually saving in Jupyter Lab with ipylab installed" 31 | " or Notebook, which auto-save the buffer during publish." 32 | ) 33 | 34 | notebook_title = meta.live.title 35 | title_error = ( 36 | f"No title! Update & {colors.bold('save')} your notebook with a title '# My" 37 | " title' in the first cell." 38 | ) 39 | 40 | if notebook_title is None: 41 | logger.error(title_error) 42 | return "no-title" 43 | 44 | nb = read_notebook(meta._filepath) # type: ignore 45 | if not check_last_cell(nb, calling_statement): 46 | raise RuntimeError("Can only publish from the last code cell of the notebook.") 47 | 48 | if not check_consecutiveness(nb): 49 | if "proceed_consecutiveness" in kwargs: 50 | decide = kwargs["proceed_consecutiveness"] 51 | elif meta.env == "test": 52 | decide = "y" 53 | else: 54 | decide = input(" Do you still want to proceed with publishing? (y/n) ") 55 | if decide != "y": 56 | logger.warning("Aborted!") 57 | return "aborted" 58 | 59 | return "checks-passed" 60 | 61 | 62 | def finalize_publish(*, calling_statement: str, version: Optional[str] = None): 63 | meta.store.version = set_version(version) 64 | meta.store.pypackage = meta.live.pypackage 65 | meta.store.user_handle = meta.live.user_handle 66 | meta.store.user_id = meta.live.user_id 67 | meta.store.user_name = meta.live.user_name 68 | logger.info( 69 | f"Set notebook version to {colors.bold(meta.store.version)} & wrote pypackages." 70 | ) 71 | meta.store.write(calling_statement=calling_statement) 72 | return None 73 | 74 | 75 | def publish( 76 | *, 77 | version: Optional[str] = None, 78 | i_confirm_i_saved: bool = False, 79 | **kwargs, 80 | ) -> Union[None, str]: 81 | """Publish the notebook. 82 | 83 | Runs these checks: 84 | 1. Checks consecutiveness, i.e., whether notebook cells were executed consecutively. 85 | 2. Checks that the notebook has a title. 86 | 3. Checks that the notebook is published from its last cell. 87 | 88 | Writes these data: 89 | 1. Sets version. 90 | 2. Stores currently imported python packages with their versions. 91 | 92 | Returns `None` upon success and an error code otherwise. 93 | 94 | Args: 95 | version: If `None`, leaves the version at its current value. Otherwise 96 | sets the version to the passed version. Consider semantic versioning. 97 | i_confirm_i_saved: Only relevant outside Jupyter Lab as a safeguard against 98 | losing the editor buffer content because of accidentally publishing. 99 | kwargs: Additional arguments for publishing. 100 | """ 101 | if "calling_statement" in kwargs: 102 | calling_statement = kwargs.pop("calling_statement") 103 | else: 104 | calling_statement = "publish(" 105 | result = run_checks_for_publish( 106 | i_confirm_i_saved=i_confirm_i_saved, 107 | calling_statement=calling_statement, 108 | **kwargs, 109 | ) 110 | if result == "checks-passed": 111 | return finalize_publish(version=version, calling_statement=calling_statement) 112 | else: 113 | return result 114 | -------------------------------------------------------------------------------- /nbproject/_schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from pathlib import Path 3 | 4 | from .dev._initialize import nbproject_id 5 | from .dev._notebook import Notebook, write_notebook 6 | 7 | 8 | def public_fields(obj): 9 | vars_props_dict = {} 10 | for key in dir(obj): 11 | if key[0] != "_": 12 | value = getattr(obj, key) 13 | if not callable(value) and value is not None: 14 | vars_props_dict[key] = value 15 | return vars_props_dict 16 | 17 | 18 | class NBRecord: 19 | def __init__(self, nb: Notebook): 20 | self._nb = nb 21 | 22 | metadata = nb.metadata 23 | if "nbproject" in metadata: 24 | self._filled = True 25 | for key, value in metadata["nbproject"].items(): 26 | setattr(self, "_" + key, value) 27 | else: 28 | self._filled = False 29 | 30 | def check_attr(self, attr): 31 | if hasattr(self, attr): 32 | return getattr(self, attr) 33 | else: 34 | self._filled = False 35 | return None 36 | 37 | def __setattr__(self, attr_name, value): 38 | if attr_name[0] != "_": 39 | present_value = self.check_attr(attr_name) 40 | if present_value != value: 41 | self.__dict__[attr_name] = value 42 | self._filled = False 43 | else: 44 | self.__dict__[attr_name] = value 45 | 46 | @property 47 | def id(self): 48 | id = self.check_attr("_id") 49 | if id is None: 50 | id = nbproject_id() 51 | self._id = id 52 | return id 53 | 54 | @property 55 | def time_init(self): 56 | time_init = self.check_attr("_time_init") 57 | if time_init is None: 58 | time_init = datetime.now(timezone.utc).isoformat() 59 | self._time_init = time_init 60 | return time_init 61 | 62 | @property 63 | def version(self): 64 | version = self.check_attr("_version") 65 | if version is None: 66 | version = "0" 67 | self._version = "0" 68 | return version 69 | 70 | @property 71 | def pypackage(self): 72 | if "pypackage" in self.__dict__: 73 | return self.__dict__["pypackage"] 74 | elif hasattr(self, "_pypackage"): 75 | return self._pypackage 76 | else: 77 | return None 78 | 79 | def write(self, nb_path: Path, overwrite: bool): 80 | nbproj_data = public_fields(self) 81 | if overwrite or not self._filled: 82 | self._nb.metadata["nbproject"] = nbproj_data 83 | write_notebook(self._nb, nb_path) 84 | self._filled = True 85 | 86 | 87 | class YAMLRecord: 88 | # this is for ordering and also ignore this when reading yaml fields 89 | _take_keys = ("time_init", "name", "version", "location") 90 | 91 | def __init__(self, nb_path: Path, nb_record: NBRecord, yaml_proj: dict): 92 | self._yaml_proj = yaml_proj 93 | self._nb_record = nb_record 94 | # load fields from the notebooks' metadata 95 | nb_record_fields = public_fields(nb_record) 96 | 97 | self._id = nb_record_fields.pop("id") 98 | 99 | for key, value in nb_record_fields.items(): 100 | setattr(self, key, value) 101 | 102 | # take field from yaml, takes precedence over nb_record 103 | if self._id in self._yaml_proj: 104 | for key, value in self._yaml_proj[self._id].items(): 105 | if key not in self._take_keys: 106 | setattr(self, key, value) 107 | 108 | # here set fields specific to yaml 109 | self.time_init = datetime.fromisoformat(self.time_init) # type: ignore 110 | self.time_init = self.time_init.strftime("%Y-%m-%d %H:%M") # type: ignore 111 | 112 | self.location = nb_path.parent.as_posix() 113 | self.name = nb_path.name 114 | 115 | def put_metadata(self): 116 | for key, value in public_fields(self).items(): 117 | if key not in self._take_keys: 118 | setattr(self._nb_record, key, value) 119 | 120 | def put_yaml(self): 121 | yaml_project = self._yaml_proj 122 | 123 | if self._id not in yaml_project: 124 | yaml_project[self._id] = {} 125 | 126 | yaml_record = yaml_project[self._id] 127 | fields = public_fields(self) 128 | for key in self._take_keys: 129 | yaml_record[key] = fields.pop(key) 130 | yaml_record.update(fields) 131 | -------------------------------------------------------------------------------- /nbproject/dev/__init__.py: -------------------------------------------------------------------------------- 1 | """Developer API. 2 | 3 | Metadata 4 | -------- 5 | 6 | .. autosummary:: 7 | :toctree: 8 | 9 | MetaLive 10 | MetaStore 11 | MetaContainer 12 | 13 | Functionality 14 | ------------- 15 | 16 | .. autosummary:: 17 | :toctree: 18 | 19 | infer_pypackages 20 | check_consecutiveness 21 | check_last_cell 22 | set_version 23 | 24 | Notebook file helpers 25 | --------------------- 26 | 27 | .. autosummary:: 28 | :toctree: 29 | 30 | Notebook 31 | initialize_metadata 32 | notebook_path 33 | read_notebook 34 | write_notebook 35 | 36 | """ 37 | from ._check_last_cell import check_last_cell 38 | from ._consecutiveness import check_consecutiveness 39 | from ._initialize import initialize_metadata 40 | from ._jupyter_communicate import notebook_path 41 | from ._meta_live import MetaLive 42 | from ._meta_store import MetaContainer, MetaStore 43 | from ._notebook import Notebook, read_notebook, write_notebook 44 | from ._pypackage import infer_pypackages 45 | from ._set_version import set_version 46 | -------------------------------------------------------------------------------- /nbproject/dev/_check_last_cell.py: -------------------------------------------------------------------------------- 1 | from ._notebook import Notebook 2 | 3 | 4 | def check_last_cell(nb: Notebook, calling_statement: str) -> bool: 5 | """Check whether code has been executed in the last cell. 6 | 7 | Args: 8 | nb: Notebook content. 9 | calling_statement: The statement that calls this function. 10 | """ 11 | last_code_cell_source = None 12 | for cell in nb.cells: 13 | cell_source = "".join(cell["source"]) 14 | if cell["cell_type"] == "code" and cell_source != "": 15 | last_code_cell_source = cell_source 16 | 17 | if last_code_cell_source is not None and calling_statement in last_code_cell_source: 18 | return True 19 | else: 20 | return False 21 | -------------------------------------------------------------------------------- /nbproject/dev/_classic_nb_commands.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from nbproject._logger import logger 4 | 5 | 6 | def _save_notebook(): 7 | try: 8 | from IPython.display import Javascript, display_javascript 9 | except ModuleNotFoundError: 10 | logger.warning("Can not import from IPython.") 11 | return None 12 | 13 | js = Javascript("IPython.notebook.save_notebook()") 14 | display_javascript(js) 15 | 16 | sleep(1) 17 | 18 | 19 | def _reload_notebook(): 20 | try: 21 | from IPython.display import Javascript, display_javascript 22 | except ModuleNotFoundError: 23 | logger.warning("Can not import from IPython.") 24 | return None 25 | 26 | js = Javascript("IPython.notebook.load_notebook(IPython.notebook.notebook_path)") 27 | display_javascript(js) 28 | -------------------------------------------------------------------------------- /nbproject/dev/_consecutiveness.py: -------------------------------------------------------------------------------- 1 | from nbproject._logger import logger 2 | 3 | from ._notebook import Notebook 4 | 5 | 6 | def check_consecutiveness(nb: Notebook, calling_statement: str = None) -> bool: 7 | """Check whether code cells have been executed consecutively. 8 | 9 | Needs to be called in the last code cell of a notebook. 10 | Otherwise raises `RuntimeError`. 11 | 12 | Returns cell transitions that violate execution at increments of 1 as a list 13 | of tuples. 14 | 15 | Args: 16 | nb: Notebook content. 17 | calling_statement: The statement that calls this function. 18 | """ 19 | cells = nb.cells 20 | 21 | violations = [] 22 | prev = 0 23 | 24 | ccount = 0 # need to initialize because notebook might note have code cells 25 | # and below, we check if ccount is None 26 | for cell in cells: 27 | cell_source = "".join(cell["source"]) 28 | if cell["cell_type"] != "code" or cell_source == "": 29 | continue 30 | 31 | if calling_statement is not None and calling_statement in cell_source: 32 | continue 33 | 34 | ccount = cell["execution_count"] 35 | if ccount is None or prev is None or ccount - prev != 1: 36 | violations.append((prev, ccount)) 37 | 38 | prev = ccount 39 | 40 | # ignore the very last code cell of the notebook 41 | # `check_consecutiveness` is being run during publish if `last_cell`` is True 42 | # hence, that cell has ccount is None 43 | if ccount is None: 44 | violations.pop() 45 | 46 | any_violations = len(violations) > 0 47 | if any_violations: 48 | logger.warning(f"cells {violations} were not run consecutively") 49 | else: 50 | logger.success("cell execution numbers increase consecutively") 51 | 52 | return not any_violations 53 | -------------------------------------------------------------------------------- /nbproject/dev/_frontend_commands.py: -------------------------------------------------------------------------------- 1 | from . import _classic_nb_commands as _clsnbk 2 | from . import _jupyter_lab_commands as _juplab 3 | 4 | 5 | def _save_notebook(env): 6 | if env == "lab": 7 | _juplab._save_notebook() 8 | elif env == "notebook": 9 | _clsnbk._save_notebook() 10 | else: 11 | raise ValueError(f"Unrecognized environment {env}.") 12 | 13 | 14 | def _reload_notebook(env): 15 | if env == "lab": 16 | _juplab._reload_notebook() 17 | elif env == "notebook": 18 | _clsnbk._reload_notebook() 19 | else: 20 | raise ValueError(f"Unrecognized environment {env}.") 21 | -------------------------------------------------------------------------------- /nbproject/dev/_initialize.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | from datetime import datetime, timezone 4 | from typing import List, Optional, Union 5 | 6 | from nbproject._logger import logger 7 | 8 | from ._lamin_communicate import lamin_user_settings 9 | from ._meta_store import MetaContainer 10 | from ._notebook import Notebook 11 | 12 | 13 | def nbproject_id(): # rename to nbproject_id also in metadata slot? 14 | """A 12-character base62 string.""" 15 | # https://github.com/laminlabs/lamin-notes/blob/main/docs/2022/ids.ipynb 16 | base62 = string.digits + string.ascii_letters.swapcase() 17 | id = "".join(secrets.choice(base62) for i in range(12)) 18 | return id 19 | 20 | 21 | def initialize_metadata( 22 | nb: Optional[Notebook] = None, 23 | pypackage: Union[str, List[str], None] = None, 24 | parent: Union[str, List[str], None] = None, 25 | ) -> MetaContainer: 26 | """Initialize nbproject metadata. 27 | 28 | Args: 29 | nb: If a notebook is provided, also infer pypackages from the notebook. 30 | pypackage: One or more python packages to track. 31 | parent: One or more nbproject ids of direct ancestors in a notebook pipeline. 32 | """ 33 | meta = MetaContainer( 34 | id=nbproject_id(), time_init=datetime.now(timezone.utc).isoformat() 35 | ) 36 | 37 | pypackage = [pypackage] if isinstance(pypackage, str) else pypackage 38 | if nb is not None and isinstance(pypackage, list): 39 | from ._pypackage import infer_pypackages 40 | 41 | try: 42 | meta.pypackage = infer_pypackages(nb, add_pkgs=pypackage, pin_versions=True) 43 | except: # noqa 44 | logger.warning("Failed to parse the notebook for python packages.") 45 | 46 | if parent is not None: 47 | meta.parent = parent 48 | 49 | user = lamin_user_settings() 50 | if user.handle is not None: 51 | meta.user_handle = user.handle 52 | if user.id is not None: 53 | meta.user_id = user.id 54 | if user.name is not None: 55 | meta.user_name = user.name 56 | 57 | return meta 58 | -------------------------------------------------------------------------------- /nbproject/dev/_jupyter_communicate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from itertools import chain 4 | from pathlib import Path, PurePath 5 | from urllib import request 6 | 7 | import orjson 8 | 9 | from nbproject._logger import logger 10 | 11 | from ._jupyter_lab_commands import _ipylab_is_installed, _lab_notebook_path 12 | 13 | DIR_KEYS = ("notebook_dir", "root_dir") 14 | 15 | 16 | def prepare_url(server: dict, query_str: str = ""): 17 | """Prepare url to query the jupyter server.""" 18 | token = server["token"] 19 | if token: 20 | query_str = f"{query_str}?token={token}" 21 | url = f"{server['url']}api/sessions{query_str}" 22 | 23 | return url 24 | 25 | 26 | def query_server(server: dict): 27 | """Query the jupyter server for sessions' info.""" 28 | # based on https://github.com/msm1089/ipynbname 29 | try: 30 | url = prepare_url(server) 31 | with request.urlopen(url) as req: 32 | return orjson.loads(req.read()) 33 | except Exception: 34 | CONN_ERROR = ( 35 | "Unable to access server;\n" 36 | "querying requires either no security or token based security." 37 | ) 38 | raise Exception(CONN_ERROR) from None 39 | 40 | 41 | def running_servers(): 42 | """Return the info about running jupyter servers.""" 43 | nbapp_import = False 44 | 45 | try: 46 | from notebook.notebookapp import list_running_servers 47 | 48 | nbapp_import = True 49 | servers_nbapp = list_running_servers() 50 | except ModuleNotFoundError: 51 | servers_nbapp = [] 52 | 53 | try: 54 | from jupyter_server.serverapp import list_running_servers 55 | 56 | servers_juserv = list_running_servers() 57 | except ModuleNotFoundError: 58 | servers_juserv = [] 59 | 60 | if not nbapp_import: 61 | logger.warning( 62 | "It looks like you are running jupyter lab " 63 | "but don't have jupyter-server module installed. " 64 | "Please install it via pip install jupyter-server" 65 | ) 66 | 67 | return servers_nbapp, servers_juserv 68 | 69 | 70 | def find_nb_path_via_parent_process(): 71 | """Tries to find the notebook path by inspecting the parent process's command line. 72 | 73 | Requires the 'psutil' library. Heuristic and potentially fragile. 74 | """ 75 | import psutil 76 | 77 | try: 78 | current_process = psutil.Process(os.getpid()) 79 | parent_process = current_process.parent() 80 | 81 | if parent_process is None: 82 | logger.warning("psutil: Could not get parent process.") 83 | return None 84 | 85 | # Get parent command line arguments 86 | cmdline = parent_process.cmdline() 87 | if not cmdline: 88 | logger.warning( 89 | f"psutil: Parent process ({parent_process.pid}) has empty cmdline." 90 | ) 91 | # Maybe check grandparent? This gets complicated quickly. 92 | return None 93 | 94 | logger.info(f"psutil: Parent cmdline: {cmdline}") 95 | 96 | # Heuristic parsing: Look for 'nbconvert' and '.ipynb' 97 | # This is fragile and depends on how nbconvert was invoked. 98 | is_nbconvert_call = False 99 | potential_path = None 100 | 101 | for i, arg in enumerate(cmdline): 102 | # Check if 'nbconvert' command is present 103 | if "nbconvert" in arg.lower(): 104 | # Check if it's the main command (e.g. /path/to/jupyter-nbconvert) 105 | # or a subcommand (e.g. ['jupyter', 'nbconvert', ...]) 106 | # or a module call (e.g. ['python', '-m', 'nbconvert', ...]) 107 | base_arg = os.path.basename(arg).lower() # noqa: PTH119 108 | if ( 109 | "jupyter-nbconvert" in base_arg 110 | or arg == "nbconvert" 111 | or ( 112 | cmdline[i - 1].endswith("python") 113 | and arg == "-m" 114 | and cmdline[i + 1] == "nbconvert" 115 | ) 116 | ): 117 | is_nbconvert_call = True 118 | 119 | # Find the argument ending in .ipynb AFTER 'nbconvert' is likely found 120 | # Or just find the last argument ending in .ipynb as a guess 121 | if arg.endswith(".ipynb"): 122 | potential_path = arg # Store the last one found 123 | 124 | if is_nbconvert_call and "--inplace" not in cmdline: 125 | raise ValueError( 126 | "Please execute notebook 'nbconvert' by passing option '--inplace'." 127 | ) 128 | 129 | if is_nbconvert_call and potential_path: 130 | # We found something that looks like an nbconvert call and an ipynb file 131 | # The path might be relative to the parent process's CWD. 132 | # Try to resolve it. Parent CWD might not be notebook dir if called like 133 | # jupyter nbconvert --execute /abs/path/to/notebook.ipynb 134 | try: 135 | # Get parent's CWD 136 | parent_cwd = parent_process.cwd() 137 | resolved_path = Path(parent_cwd) / Path(potential_path) 138 | if resolved_path.is_file(): 139 | logger.info(f"psutil: Found potential path: {resolved_path}") 140 | return resolved_path.resolve() # Return absolute path 141 | else: 142 | # Maybe the path was already absolute? 143 | abs_path = Path(potential_path) 144 | if abs_path.is_absolute() and abs_path.is_file(): 145 | logger.info( 146 | f"psutil: Found potential absolute path: {abs_path}" 147 | ) 148 | return abs_path.resolve() 149 | else: 150 | logger.warning( 151 | f"psutil: Potential path '{potential_path}' not found relative to parent CWD '{parent_cwd}' or as absolute path." 152 | ) 153 | return None 154 | 155 | except psutil.AccessDenied: 156 | logger.warning("psutil: Access denied when getting parent CWD.") 157 | # Fallback: assume path might be relative to kernel's CWD (less likely) 158 | maybe_path = Path(potential_path) 159 | if maybe_path.is_file(): 160 | return maybe_path.resolve() 161 | return None # Give up trying to resolve relative path 162 | except Exception as e: 163 | logger.warning(f"psutil: Error resolving path '{potential_path}': {e}") 164 | return None 165 | 166 | logger.warning( 167 | "psutil: Could not reliably identify notebook path from parent cmdline." 168 | ) 169 | return None 170 | 171 | except ImportError: 172 | logger.warning("psutil library not found. Cannot inspect parent process.") 173 | return None 174 | except psutil.Error as e: 175 | logger.warning(f"psutil error: {e}") 176 | return None 177 | except ValueError as ve: # Explicitly catch and re-raise the intended error 178 | raise ve 179 | except Exception as e: 180 | logger.warning(f"Unexpected error during psutil check: {e}") 181 | return None 182 | 183 | 184 | def notebook_path(return_env=False): 185 | """Return the path to the current notebook. 186 | 187 | Args: 188 | return_env: If `True`, return the environment of execution: 189 | `'lab'` for jupyter lab and `'notebook'` for jupyter notebook. 190 | """ 191 | env = None 192 | if "NBPRJ_TEST_NBENV" in os.environ: 193 | env = os.environ["NBPRJ_TEST_NBENV"] 194 | 195 | if "NBPRJ_TEST_NBPATH" in os.environ: 196 | nb_path = os.environ["NBPRJ_TEST_NBPATH"] 197 | if return_env: 198 | return nb_path, "test" if env is None else env 199 | else: 200 | return nb_path 201 | 202 | try: 203 | from IPython import get_ipython 204 | except ModuleNotFoundError: 205 | logger.warning("Can not import get_ipython.") 206 | return None 207 | 208 | # vs code specific 209 | if "__main__" in sys.modules: 210 | main_module = sys.modules["__main__"] 211 | if hasattr(main_module, "__vsc_ipynb_file__"): 212 | nb_path = main_module.__vsc_ipynb_file__ 213 | return ( 214 | (nb_path, "vs_code" if env is None else env) if return_env else nb_path 215 | ) 216 | 217 | ipython_instance = get_ipython() 218 | 219 | # not in an ipython kernel 220 | if ipython_instance is None: 221 | logger.warning("The IPython instance is empty.") 222 | return None 223 | 224 | config = ipython_instance.config 225 | # not in a jupyter notebook 226 | if "IPKernelApp" not in config: 227 | logger.warning("IPKernelApp is not in ipython_instance.config.") 228 | return None 229 | 230 | kernel_id = ( 231 | config["IPKernelApp"]["connection_file"].partition("-")[2].split(".", -1)[0] 232 | ) 233 | 234 | servers_nbapp, servers_juserv = running_servers() 235 | 236 | server_exception = None 237 | 238 | for server in chain(servers_nbapp, servers_juserv): 239 | try: 240 | session = query_server(server) 241 | except Exception as e: 242 | server_exception = e 243 | continue 244 | 245 | for notebook in session: 246 | if "kernel" not in notebook or "notebook" not in notebook: 247 | continue 248 | if notebook["kernel"].get("id", None) == kernel_id: 249 | for dir_key in DIR_KEYS: 250 | if dir_key in server: 251 | nb_path = ( 252 | PurePath(server[dir_key]) / notebook["notebook"]["path"] 253 | ) 254 | 255 | if return_env: 256 | if env is None: 257 | rt_env = "lab" if dir_key == "root_dir" else "notebook" 258 | else: 259 | rt_env = env 260 | return nb_path, rt_env 261 | else: 262 | return nb_path 263 | 264 | # trying to get the path through ipylab 265 | nb_path = _lab_notebook_path() 266 | if nb_path is not None: 267 | return (nb_path, "lab" if env is None else env) if return_env else nb_path 268 | 269 | # for newer versions of lab, less safe as it stays the same after file rename 270 | if "JPY_SESSION_NAME" in os.environ: 271 | nb_path = PurePath(os.environ["JPY_SESSION_NAME"]) 272 | return (nb_path, "lab" if env is None else env) if return_env else nb_path 273 | 274 | # try inspecting parent process using psutil, needed if notebook is run via nbconvert 275 | nb_path_psutil = find_nb_path_via_parent_process() 276 | if nb_path_psutil is not None: 277 | logger.info("Detected path via psutil parent process inspection.") 278 | return ( 279 | (nb_path_psutil, "nbconvert" if env is None else env) 280 | if return_env 281 | else nb_path_psutil 282 | ) 283 | 284 | # no running servers 285 | if servers_nbapp == [] and servers_juserv == []: 286 | logger.warning("Can not find any servers running.") 287 | 288 | logger.warning( 289 | "Can not find the notebook in any server session or by using other methods." 290 | ) 291 | if not _ipylab_is_installed(): 292 | logger.warning( 293 | "Consider installing ipylab (pip install ipylab) if you use jupyter lab." 294 | ) 295 | 296 | if server_exception is not None: 297 | raise server_exception 298 | 299 | return None 300 | -------------------------------------------------------------------------------- /nbproject/dev/_jupyter_lab_commands.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from time import sleep 3 | 4 | from nbproject._is_run_from_ipython import is_run_from_ipython 5 | 6 | app = None 7 | 8 | 9 | # called in __init__.py 10 | def _init_frontend(): 11 | try: 12 | from ipylab import JupyterFrontEnd 13 | 14 | global app 15 | if app is None and is_run_from_ipython: 16 | app = JupyterFrontEnd() 17 | except ImportError: 18 | pass 19 | 20 | 21 | def _save_notebook(): 22 | if app is not None: 23 | app.commands.execute("docmanager:save") 24 | sleep(1) 25 | 26 | 27 | def _reload_notebook(): 28 | if app is not None: 29 | app.commands.execute("docmanager:reload") 30 | 31 | 32 | def _lab_notebook_path(): 33 | if app is None: 34 | return None 35 | 36 | current_session = app.sessions.current_session 37 | 38 | if "name" in current_session: 39 | nb_path = Path.cwd() / app.sessions.current_session["name"] 40 | else: 41 | nb_path = None 42 | 43 | return nb_path 44 | 45 | 46 | def _ipylab_is_installed(): 47 | if app is not None: 48 | return True 49 | try: 50 | import ipylab 51 | 52 | return True 53 | except ImportError: 54 | return False 55 | -------------------------------------------------------------------------------- /nbproject/dev/_lamin_communicate.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | def lamin_user_settings(): 5 | """Returns user settings.""" 6 | try: 7 | from lndb import settings 8 | 9 | return settings.user 10 | except ImportError: 11 | MockUserSettings = namedtuple("MockUserSettings", ["id", "handle", "name"]) 12 | return MockUserSettings(id=None, handle=None, name=None) 13 | -------------------------------------------------------------------------------- /nbproject/dev/_meta_live.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from pathlib import Path 3 | from typing import Optional, Union 4 | 5 | from nbproject._logger import logger 6 | 7 | from ._consecutiveness import check_consecutiveness 8 | from ._jupyter_lab_commands import _save_notebook 9 | from ._lamin_communicate import lamin_user_settings 10 | from ._notebook import Notebook, read_notebook 11 | from ._pypackage import infer_pypackages 12 | 13 | 14 | def get_title(nb: Notebook) -> Optional[str]: 15 | """Get title of the notebook.""" 16 | # loop through all cells 17 | for cell in nb.cells: 18 | # only consider markdown 19 | if cell["cell_type"] == "markdown": 20 | # grab source 21 | text = "".join(cell["source"]) 22 | # loop through lines 23 | for line in text.split("\n"): 24 | # if finding a level-1 heading, consider it a title 25 | if line.startswith("# "): 26 | title = line.lstrip("#").strip(" .").strip("\n") 27 | return title 28 | return None 29 | 30 | 31 | class MetaLive: 32 | """Live properties of the notebook. 33 | 34 | All attributes represent either the execution information or properties inferred 35 | on access from the notebook's content. 36 | """ 37 | 38 | def __init__( 39 | self, 40 | nb_path: Union[str, Path], 41 | time_run: Optional[datetime] = None, 42 | env: Optional[str] = None, 43 | ): 44 | self._nb_path = nb_path 45 | self._env = env 46 | self._time_run = time_run 47 | 48 | @property 49 | def title(self) -> Optional[str]: 50 | """Get the title of the notebook. 51 | 52 | The first cell should contain markdown text formatted as a title. 53 | """ 54 | nb = read_notebook(self._nb_path) 55 | return get_title(nb) 56 | 57 | @property 58 | def pypackage(self): 59 | """Infer pypackages for the notebook. 60 | 61 | This accounts for additional pypackages in the file metadata. 62 | """ 63 | nb = read_notebook(self._nb_path) 64 | add_pkgs = None 65 | if "nbproject" in nb.metadata and "pypackage" in nb.metadata["nbproject"]: 66 | if nb.metadata["nbproject"]["pypackage"] is not None: 67 | add_pkgs = nb.metadata["nbproject"]["pypackage"].keys() 68 | return infer_pypackages(nb, add_pkgs, pin_versions=True) 69 | 70 | @property 71 | def consecutive_cells(self) -> bool: 72 | """Have notebook cells been consecutively executed? 73 | 74 | Logs cell transitions that violate execution at increments of 1 75 | as a list of tuples. 76 | """ 77 | if self._env == "lab": 78 | _save_notebook() 79 | elif self._env != "test": 80 | logger.info("Save the notebook before checking for consecutiveness.") 81 | nb = read_notebook(self._nb_path) 82 | consecutiveness = check_consecutiveness( 83 | nb, calling_statement=".live.consecutive_cells" 84 | ) 85 | return consecutiveness 86 | 87 | @property 88 | def time_run(self): 89 | """The time when the current session started. 90 | 91 | To get the proper time run, you need to use `from nbproject import header` 92 | at the beginning of the notebook. Otherwise, the time run is set to the time 93 | of the first access to this attribute. 94 | """ 95 | if self._time_run is None: 96 | self._time_run = datetime.now(timezone.utc) 97 | return self._time_run.isoformat() 98 | 99 | @property 100 | def time_passed(self): 101 | """Number of seconds elapsed from `time_run`.""" 102 | return (datetime.now(timezone.utc) - self._time_run).total_seconds() 103 | 104 | @property 105 | def user_handle(self): 106 | """User handle from lamindb.""" 107 | return lamin_user_settings().handle 108 | 109 | @property 110 | def user_id(self): 111 | """User ID from lamindb.""" 112 | return lamin_user_settings().id 113 | 114 | @property 115 | def user_name(self): 116 | """User name from lamindb.""" 117 | return lamin_user_settings().name 118 | 119 | def __repr__(self): 120 | return "Fields: " + " ".join([key for key in dir(self) if key[0] != "_"]) 121 | -------------------------------------------------------------------------------- /nbproject/dev/_meta_store.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List, Mapping, Optional, Union 3 | 4 | from pydantic import BaseModel, ConfigDict 5 | 6 | from nbproject._logger import logger 7 | 8 | from ._frontend_commands import _reload_notebook, _save_notebook 9 | from ._metadata_display import table_metadata 10 | from ._notebook import Notebook, read_notebook, write_notebook 11 | from ._pypackage import _get_version 12 | 13 | 14 | def _change_display_table(metadata: Mapping, notebook: Notebook): 15 | cells = notebook.cells 16 | 17 | table_html = table_metadata(metadata, notebook) 18 | 19 | for cell in cells: 20 | if cell["cell_type"] == "code": 21 | for out in cell["outputs"]: 22 | if "data" in out and "text/html" in out["data"]: 23 | html = out["data"]["text/html"][0] 24 | if "version" in html: 25 | out["data"]["text/html"][0] = table_html 26 | break 27 | 28 | 29 | def _set_execution_count(calling_statement: str, notebook: Notebook): 30 | cells = notebook.cells 31 | 32 | prev = 0 33 | for cell in cells: 34 | cell_source = "".join(cell["source"]) 35 | 36 | if cell["cell_type"] != "code" or cell_source == "": 37 | continue 38 | 39 | if calling_statement in cell_source: 40 | cell["execution_count"] = prev + 1 41 | 42 | ccount = cell["execution_count"] 43 | if ccount is not None: 44 | prev = ccount 45 | 46 | 47 | class MetaContainer(BaseModel): 48 | """The metadata stored in the notebook file.""" 49 | 50 | id: str 51 | """A universal 8-digit base62 ID.""" 52 | version: str = "1" 53 | """Published version of notebook.""" 54 | time_init: str 55 | """Time of nbproject init in UTC. Often coincides with notebook creation.""" 56 | pypackage: Optional[Mapping[str, str]] = None 57 | """Dictionary of notebook pypackages and their versions.""" 58 | parent: Union[str, List[str], None] = None 59 | """One or more nbproject ids of direct ancestors in a notebook pipeline.""" 60 | user_handle: Optional[str] = None 61 | """User handle from lamindb.""" 62 | user_id: Optional[str] = None 63 | """User ID from lamindb.""" 64 | user_name: Optional[str] = None 65 | """User name from lamindb.""" 66 | model_config = ConfigDict(extra="allow") 67 | 68 | 69 | class MetaStore: 70 | """The wrapper class for metadata stored in the notebook file.""" 71 | 72 | def __init__( 73 | self, 74 | meta_container: MetaContainer, 75 | filepath: Union[str, Path, None] = None, 76 | env: Optional[str] = None, 77 | ): 78 | self._filepath = filepath 79 | self._env = env 80 | 81 | self._meta_container = meta_container 82 | 83 | def __getattr__(self, attr_name): 84 | return getattr(self._meta_container, attr_name) 85 | 86 | def __setattr__(self, attr_name, value): 87 | if attr_name[0] != "_": 88 | setattr(self._meta_container, attr_name, value) 89 | else: 90 | self.__dict__[attr_name] = value 91 | 92 | def add_pypackages(self, packages: Union[List[str], str]) -> "MetaStore": 93 | """Manually add pypackages to track. 94 | 95 | Pass a string or a list of strings representing package names. 96 | 97 | Returns self. 98 | """ 99 | if self._meta_container.pypackage is None: 100 | self._meta_container.pypackage = {} 101 | 102 | deps_dict = self._meta_container.pypackage 103 | 104 | if isinstance(packages, str): 105 | packages = [packages] 106 | 107 | for dep in packages: 108 | if dep not in deps_dict: 109 | deps_dict[dep] = _get_version(dep) # type: ignore 110 | return self 111 | 112 | def write(self, **kwargs): 113 | """Write to file. 114 | 115 | You can edit the nbproject metadata of the current notebook 116 | by changing `.store` fields and then using this function 117 | to write the changes to the file. 118 | 119 | Outside Jupyter Lab: Save the notebook before writing. 120 | """ 121 | if self._env in ("lab", "notebook"): 122 | _save_notebook(self._env) 123 | 124 | nb = read_notebook(self._filepath) 125 | 126 | upd_metadata = self._meta_container.dict() 127 | nb.metadata["nbproject"] = upd_metadata 128 | 129 | _change_display_table(upd_metadata, nb) 130 | 131 | if "calling_statement" in kwargs: 132 | _set_execution_count(kwargs["calling_statement"], nb) 133 | 134 | write_notebook(nb, self._filepath) 135 | 136 | if self._env in ("lab", "notebook"): 137 | _reload_notebook(self._env) 138 | elif self._env != "test": 139 | logger.info( 140 | "File changed on disk! Reload the notebook if you want to continue." 141 | ) 142 | # sys.exit(0) # makes CI fail, need to think of a decent way of exiting 143 | 144 | def __repr__(self): 145 | return f"Wrapper object for the stored metadata:\n {self._meta_container}" 146 | -------------------------------------------------------------------------------- /nbproject/dev/_metadata_display.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime, timezone 2 | from enum import Enum 3 | from typing import Mapping, Optional 4 | 5 | from pydantic import BaseModel 6 | 7 | from nbproject._logger import logger 8 | 9 | from ._consecutiveness import check_consecutiveness 10 | from ._lamin_communicate import lamin_user_settings 11 | from ._notebook import Notebook 12 | from ._pypackage import infer_pypackages 13 | 14 | 15 | def table_html(rows: list): 16 | html = "" 17 | for row in rows: 18 | html += "" 19 | html += f"" 20 | for col in row: 21 | html += f"" 22 | html += "" 23 | html += "
{row.pop(0)}{col}
" 24 | return html 25 | 26 | 27 | def display_html(html: str): 28 | from IPython.display import HTML, display 29 | 30 | display(HTML(html)) 31 | 32 | 33 | # display configuration 34 | class DisplayConf(BaseModel): 35 | time_init: Enum("choice", ["date", "datetime"]) = "datetime" # type: ignore 36 | time_run: Enum("choice", ["date", "datetime"]) = "datetime" # type: ignore 37 | 38 | 39 | def color_id(id: str): 40 | return f"{id[:4]}{id[4:]}" 41 | 42 | 43 | # displays fields within the ipynb metadata section and on-the-fly computed 44 | class DisplayMeta: 45 | def __init__(self, metadata: Mapping): 46 | self.metadata = metadata 47 | self.conf = DisplayConf() 48 | 49 | def id(self): 50 | """Shorten ID display.""" 51 | id = self.metadata["id"] if "id" in self.metadata else self.metadata["uid"] 52 | return color_id(id) 53 | 54 | def version(self): 55 | return self.metadata["version"] 56 | 57 | def parent(self): 58 | if "parent" in self.metadata: 59 | parent = self.metadata["parent"] 60 | if parent is None: 61 | return None 62 | if isinstance(parent, list): 63 | return " ".join([color_id(id) for id in parent]) 64 | else: 65 | return color_id(parent) 66 | else: 67 | return None 68 | 69 | def time_init(self): 70 | """Shorten ID display.""" 71 | dt = datetime.fromisoformat(self.metadata["time_init"]) 72 | if self.conf.time_init == "date": 73 | return dt.date() 74 | else: 75 | return dt.strftime( 76 | "%Y-%m-%d %H:%M" 77 | ) # probably something more reduced is better 78 | 79 | # this is not part of the ipynb metadata section 80 | def time_run(self, dt: datetime): 81 | """Shorten ID display.""" 82 | if self.conf.time_run == "date": 83 | return dt.date() 84 | else: 85 | return dt.strftime( 86 | "%Y-%m-%d %H:%M" 87 | ) # probably something more reduced is better 88 | 89 | def pypackage(self, deps: Optional[Mapping] = None): 90 | if deps is None and "pypackage" in self.metadata: 91 | deps = self.metadata["pypackage"] 92 | 93 | if deps is None: 94 | return None 95 | 96 | deps_list = [] 97 | for pkg, ver in deps.items(): 98 | if ver != "": 99 | deps_list.append(pkg + f"=={ver}") 100 | else: 101 | deps_list.append(pkg) 102 | 103 | if deps_list == []: 104 | return None 105 | else: 106 | deps_list.sort() 107 | return deps_list 108 | 109 | def author(self): 110 | user_handle = self.metadata.get("user_handle", None) 111 | user_id = self.metadata.get("user_id", None) 112 | user_name = self.metadata.get("user_name", None) 113 | 114 | if any((user_handle is None, user_id is None, user_name is None)): 115 | user = lamin_user_settings() 116 | user_handle, user_id, user_name = user.handle, user.id, user.name 117 | 118 | # preferred display 119 | if user_name is not None and user_handle is not None: 120 | return f"{user_name} ({user_handle})" 121 | # fallback display (only handle) 122 | elif user_handle is not None: 123 | return f"{user_handle}" 124 | 125 | return None 126 | 127 | 128 | def table_metadata( 129 | metadata: Mapping, notebook: Notebook, time_run: Optional[datetime] = None 130 | ): 131 | dm = DisplayMeta(metadata) 132 | 133 | table = [] 134 | 135 | author = dm.author() 136 | if author is not None: 137 | table.append(["author", author]) 138 | 139 | table.append(["id", dm.id()]) 140 | version = dm.version() 141 | table.append(["version", version]) 142 | 143 | table.append(["time_init", dm.time_init()]) 144 | 145 | if time_run is None: 146 | time_run = datetime.now(timezone.utc) 147 | table.append(["time_run", dm.time_run(time_run)]) 148 | 149 | if dm.parent() is not None: 150 | table.append(["parent", dm.parent()]) 151 | 152 | if version not in {"0", "1"}: 153 | with logger.mute(): 154 | consecutiveness = check_consecutiveness(notebook) 155 | table.append(["consecutive_cells", str(consecutiveness)]) 156 | 157 | dep_store = dm.pypackage() 158 | if dep_store is not None: 159 | add_pkgs = [pkg.partition("==")[0] for pkg in dep_store] 160 | else: 161 | add_pkgs = None 162 | dep_live = dm.pypackage(infer_pypackages(notebook, add_pkgs, pin_versions=True)) 163 | 164 | # simplify display when stored & live pypackages match 165 | if dep_store is not None and dep_live is not None and dep_live == dep_store: 166 | table.append(["pypackage", " ".join(dep_store)]) 167 | else: 168 | if dep_store is not None: 169 | table.append(["pypackage_store", " ".join(dep_store)]) 170 | suffix = "_live" 171 | else: 172 | suffix = "" 173 | if dep_live is not None: 174 | table.append([f"pypackage{suffix}", " ".join(dep_live)]) 175 | 176 | return table_html(table) 177 | -------------------------------------------------------------------------------- /nbproject/dev/_notebook.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | 4 | import orjson 5 | from pydantic import BaseModel 6 | 7 | 8 | class Notebook(BaseModel): 9 | """Jupyter notebook model.""" 10 | 11 | metadata: dict 12 | nbformat: int 13 | nbformat_minor: int 14 | cells: list 15 | 16 | 17 | def read_notebook(filepath: Union[str, Path]) -> Notebook: 18 | """Read a notebook from disk. 19 | 20 | Args: 21 | filepath: A path to the notebook to read. 22 | """ 23 | with open(filepath, "rb") as f: 24 | try: 25 | nb = orjson.loads(f.read()) 26 | except orjson.JSONDecodeError as e: 27 | if "Input is a zero-length, empty document" in str(e): 28 | raise ValueError("Notebook cannot be empty. It must have at least a title cell.") 29 | else: 30 | raise 31 | 32 | 33 | return Notebook(**nb) 34 | 35 | 36 | def write_notebook(nb: Notebook, filepath: Union[str, Path]): 37 | """Write the notebook to disk. 38 | 39 | Args: 40 | nb: Notebook to write. 41 | filepath: Path where to write the notebook. 42 | """ 43 | with open(filepath, "wb") as f: 44 | # the formatting orjson dumps doesn't match jupyter lab 45 | # maybe one can homogenize it at some point 46 | f.write(orjson.dumps(nb.dict(), option=orjson.OPT_INDENT_2)) 47 | -------------------------------------------------------------------------------- /nbproject/dev/_pypackage.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from ast import Import, ImportFrom, parse, walk 4 | from typing import Iterable, List, Optional, Union 5 | 6 | from importlib_metadata import PackageNotFoundError, packages_distributions, version 7 | 8 | from ._notebook import Notebook 9 | 10 | std_libs = None 11 | pkgs_dists = None 12 | 13 | 14 | def _load_pkgs_info(): 15 | global std_libs 16 | global pkgs_dists 17 | 18 | major, minor = sys.version_info[0], sys.version_info[1] 19 | if major == 3 and minor > 9: 20 | std_libs = sys.stdlib_module_names # type: ignore 21 | else: 22 | from stdlib_list import stdlib_list 23 | 24 | std_libs = set(stdlib_list(f"{major}.{minor}")) 25 | 26 | pkgs_dists = packages_distributions() 27 | 28 | 29 | def _get_version(pkg): 30 | try: 31 | pkg_ver = version(pkg) 32 | except PackageNotFoundError: 33 | pkg_ver = "" 34 | return pkg_ver 35 | 36 | 37 | def cell_imports(cell_source: str): 38 | # based on the package https://github.com/bndr/pipreqs for python scripts 39 | # parses python import statements in the code cells 40 | tree = parse(cell_source) 41 | for node in walk(tree): 42 | if isinstance(node, Import): 43 | for subnode in node.names: 44 | name = subnode.name.partition(".")[0] 45 | if name != "": 46 | yield name 47 | elif isinstance(node, ImportFrom): 48 | name = node.module.partition(".")[0] # type: ignore 49 | if name != "": 50 | yield name 51 | 52 | 53 | def infer_pypackages( 54 | content: Notebook, 55 | add_pkgs: Optional[Iterable] = None, 56 | pin_versions: bool = True, 57 | ): 58 | """Parse notebook object and infer all pypackages. 59 | 60 | This does not account for additional packages in file metadata. 61 | 62 | For the user-facing functionality, 63 | see :meth:`~nbproject.dev.MetaLive.pypackage`. 64 | 65 | Args: 66 | content: A notebook to infer pypackages from. 67 | add_pkgs: Additional packages to add. 68 | pin_versions: If `True`, fixes versions from the current environment. 69 | 70 | Examples: 71 | >>> pypackages = nbproject.dev.infer_pypackages(nb) 72 | >>> pypackages 73 | {"scanpy": "1.8.7", "pandas": "1.4.3"} 74 | """ 75 | cells = content.cells 76 | 77 | if std_libs is None or pkgs_dists is None: 78 | _load_pkgs_info() 79 | 80 | pkgs = set() 81 | magics_re = None 82 | 83 | for cell in cells: 84 | if cell["cell_type"] != "code": 85 | continue 86 | 87 | # assuming we read the notebook with a json reader 88 | cell_source = "".join(cell["source"]) 89 | if "import" not in cell_source: 90 | continue 91 | else: 92 | # quick hack to ignore jupyter magics 93 | if "%" in cell_source: 94 | if magics_re is None: 95 | magics_re = re.compile(r"^( *)%{1,2}\w+ *", flags=re.MULTILINE) 96 | cell_source = magics_re.sub(r"\1", cell_source) 97 | 98 | for imp in cell_imports(cell_source): 99 | if imp in std_libs: # type: ignore 100 | continue 101 | if imp in pkgs_dists: # type: ignore 102 | pkgs.update(pkgs_dists[imp]) # type: ignore 103 | else: 104 | pkgs.add(imp) 105 | 106 | if add_pkgs is not None: 107 | pkgs.update(add_pkgs) 108 | 109 | pkgs = {pkg: "" for pkg in pkgs} # type: ignore 110 | if not pin_versions: 111 | return pkgs 112 | 113 | for pkg in pkgs: 114 | pkgs[pkg] = _get_version(pkg) # type: ignore 115 | 116 | return pkgs 117 | 118 | 119 | def _resolve(a, b): 120 | import packaging.version 121 | 122 | parse_version = packaging.version.parse 123 | 124 | if a == "": 125 | return b 126 | elif b == "": 127 | return a 128 | else: 129 | return a if parse_version(a) > parse_version(b) else b 130 | 131 | 132 | def resolve_versions(notebooks_pkgs: List[dict]): 133 | """Harmonize packages' versions from lists of packages.""" 134 | resolved = {} 135 | for pkgs in notebooks_pkgs: 136 | for pkg, ver in pkgs.items(): 137 | if pkg not in resolved: 138 | resolved[pkg] = ver 139 | else: 140 | resolved[pkg] = _resolve(resolved[pkg], ver) 141 | 142 | return resolved 143 | -------------------------------------------------------------------------------- /nbproject/dev/_set_version.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from nbproject._meta import meta 4 | 5 | 6 | def set_version( 7 | version: Union[str, None] = None, stored_version: Union[str, None] = None 8 | ): 9 | """(Auto-) set version. 10 | 11 | If `version` is `None`, returns the stored version. 12 | Otherwise sets the version to the passed version. 13 | 14 | Args: 15 | version: Version string. 16 | stored_version: Mock stored version for testing purposes. 17 | """ 18 | if stored_version is None: 19 | stored_version = meta.store.version 20 | 21 | if version is not None: 22 | return version 23 | else: 24 | return stored_version 25 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | from laminci.nox import build_docs, run_pre_commit, run_pytest 3 | 4 | 5 | @nox.session 6 | def lint(session: nox.Session) -> None: 7 | run_pre_commit(session) 8 | 9 | 10 | @nox.session 11 | def build(session): 12 | session.run(*"pip install .[dev]".split()) 13 | run_pytest(session) 14 | build_docs(session) 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "nbproject" 7 | authors = [{name = "Lamin Labs", email = "laminlabs@gmail.com"}] 8 | readme = "README.md" 9 | dynamic = ["version", "description"] 10 | classifiers = [ 11 | "License :: OSI Approved :: Apache Software License", 12 | "Programming Language :: Python :: 3.8", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | ] 17 | dependencies = [ 18 | "pydantic>=2.0.0", 19 | "pyyaml", 20 | "packaging", 21 | "orjson", 22 | "psutil", 23 | "importlib-metadata", 24 | "stdlib_list; python_version < '3.10'", 25 | "lamin_utils>=0.13.2", 26 | ] 27 | 28 | [project.urls] 29 | Home = "https://github.com/laminlabs/nbproject" 30 | 31 | [project.optional-dependencies] 32 | dev = [ 33 | "pre-commit", 34 | "black", 35 | "pytest>=6.0", 36 | "pytest-cov", 37 | "pandas", 38 | "nbformat", 39 | "nbproject_test >= 0.4.5", 40 | "laminci", 41 | "ipylab", 42 | "nbconvert", 43 | ] 44 | 45 | [project.scripts] 46 | nbproject = "nbproject.__main__:main" 47 | 48 | [tool.pytest.ini_options] 49 | testpaths = [ 50 | "tests", 51 | ] 52 | 53 | [tool.coverage.run] 54 | omit = [ 55 | "nbproject/*", 56 | ] 57 | 58 | 59 | [tool.ruff] 60 | src = ["src"] 61 | line-length = 88 62 | select = [ 63 | "F", # Errors detected by Pyflakes 64 | "E", # Error detected by Pycodestyle 65 | "W", # Warning detected by Pycodestyle 66 | "I", # isort 67 | "D", # pydocstyle 68 | "B", # flake8-bugbear 69 | "TID", # flake8-tidy-imports 70 | "C4", # flake8-comprehensions 71 | "BLE", # flake8-blind-except 72 | "UP", # pyupgrade 73 | "RUF100", # Report unused noqa directives 74 | "TCH", # Typing imports 75 | "NPY", # Numpy specific rules 76 | "PTH" # Use pathlib 77 | ] 78 | ignore = [ 79 | # Do not catch blind exception: `Exception` 80 | "BLE001", 81 | # Errors from function calls in argument defaults. These are fine when the result is immutable. 82 | "B008", 83 | # line too long -> we accept long comment lines; black gets rid of long code lines 84 | "E501", 85 | # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient 86 | "E731", 87 | # allow I, O, l as variable names -> I is the identity matrix 88 | "E741", 89 | # Missing docstring in public module 90 | "D100", 91 | # undocumented-public-class 92 | "D101", 93 | # Missing docstring in public method 94 | "D102", 95 | # Missing docstring in public function 96 | "D103", 97 | # Missing docstring in public package 98 | "D104", 99 | # __magic__ methods are are often self-explanatory, allow missing docstrings 100 | "D105", 101 | # Missing docstring in public nested class 102 | "D106", 103 | # Missing docstring in __init__ 104 | "D107", 105 | ## Disable one in each pair of mutually incompatible rules 106 | # We don’t want a blank line before a class docstring 107 | "D203", 108 | # 1 blank line required after class docstring 109 | "D204", 110 | # first line should end with a period [Bug: doesn't work with single-line docstrings] 111 | # We want docstrings to start immediately after the opening triple quote 112 | "D213", 113 | # Section underline is over-indented ("{name}") 114 | "D215", 115 | # First line should end with a period 116 | "D400", 117 | # First line should be in imperative mood; try rephrasing 118 | "D401", 119 | # First word of the first line should be capitalized: {} -> {} 120 | "D403", 121 | # First word of the docstring should not be "This" 122 | "D404", 123 | # Section name should end with a newline ("{name}") 124 | "D406", 125 | # Missing dashed underline after section ("{name}") 126 | "D407", 127 | # Section underline should be in the line following the section's name ("{name}") 128 | "D408", 129 | # Section underline should match the length of its name ("{name}") 130 | "D409", 131 | # No blank lines allowed between a section header and its content ("{name}") 132 | "D412", 133 | # Missing blank line after last section ("{name}") 134 | "D413", 135 | # Imports unused 136 | "F401", 137 | # camcelcase imported as lowercase 138 | "N813", 139 | # module import not at top level of file 140 | "E402", 141 | # open()` should be replaced by `Path.open() 142 | "PTH123", 143 | ] 144 | 145 | [tool.ruff.pydocstyle] 146 | convention = "google" 147 | 148 | [tool.ruff.per-file-ignores] 149 | "docs/*" = ["I"] 150 | "tests/*" = ["D"] 151 | "*/__init__.py" = ["F401"] 152 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def pytest_collection_modifyitems(session, config, items): 5 | # make sure that cli is executed first 6 | cli_idx = 0 7 | for i, item in enumerate(items): 8 | if item.name == "test_cli": 9 | cli_idx = i 10 | 11 | # if not in the list or the first already, then ignore 12 | if cli_idx != 0: 13 | items[0], items[cli_idx] = items[cli_idx], items[0] 14 | 15 | 16 | def pytest_sessionfinish(session, exitstatus): 17 | test_cli_folder = Path(__file__).parents[1] / "docs/faq/" 18 | 19 | nbproj_file = ( 20 | test_cli_folder / "example-project-uninitialized/nbproject_metadata.yml" 21 | ) 22 | if nbproj_file.is_file(): 23 | nbproj_file.unlink() 24 | 25 | reqs_subfolders = ["example-project-uninitialized/", "example-project/"] 26 | for reqs_subfolder in reqs_subfolders: 27 | reqs_file = test_cli_folder / (reqs_subfolder + "requirments.txt") 28 | if reqs_file.is_file(): 29 | reqs_file.unlink() 30 | -------------------------------------------------------------------------------- /tests/for-nbconvert.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from nbproject.dev._jupyter_communicate import notebook_path\n", 10 | "\n", 11 | "assert notebook_path() is not None, \"Cannot infer notebook path.\"" 12 | ] 13 | } 14 | ], 15 | "metadata": { 16 | "language_info": { 17 | "name": "python" 18 | } 19 | }, 20 | "nbformat": 4, 21 | "nbformat_minor": 2 22 | } 23 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from subprocess import PIPE, Popen 3 | from typing import Optional, Sequence 4 | 5 | from nbproject._logger import logger 6 | from nbproject._schemas import NBRecord, public_fields 7 | from nbproject.dev import read_notebook, write_notebook 8 | 9 | 10 | def check_notebooks(nb_folder: Path, cleanup: Optional[Sequence] = None): 11 | if cleanup is None: 12 | cleanup = [] 13 | 14 | notebooks = nb_folder.glob("**/*.ipynb") 15 | 16 | for nb in notebooks: 17 | nb_content = read_notebook(nb) 18 | 19 | nb_record = NBRecord(nb_content) 20 | if not nb_record._filled: 21 | raise Exception(f"No nbproject metadata present in {nb}.") 22 | 23 | fields = public_fields(nb_record) 24 | nbproj_metadata = nb_content.metadata["nbproject"] 25 | for field in fields: 26 | if field not in nbproj_metadata: 27 | raise Exception(f"No field {field} in the nbproject metadata.") 28 | 29 | if nb.name in cleanup: 30 | del nb_content.metadata["nbproject"] 31 | write_notebook(nb_content, nb) 32 | 33 | 34 | def test_cli(): 35 | main_folder = Path(__file__).parents[1] / "docs" 36 | folders = ["faq/example-project-uninitialized", "faq/example-project"] 37 | 38 | commands = { 39 | "sync_noinit": ["sync", "."], 40 | "reqs_noinit": ["reqs", "."], 41 | "init": ["init"], 42 | "sync": ["sync", "."], 43 | "sync_list": ["sync"], 44 | "sync_d_nv": ["sync", ".", "-d", "-nv"], 45 | "sync_d": ["sync", ".", "-d"], 46 | "reqs_list": ["reqs"], 47 | "reqs": ["reqs", "."], 48 | "publish": ["publish", "."], 49 | } 50 | 51 | for folder in folders: 52 | nb_folder = main_folder / folder 53 | logger.debug(f"\n{nb_folder}") 54 | 55 | for cmd_name, cmd in commands.items(): 56 | if "list" in cmd_name: 57 | files = [str(file) for file in nb_folder.glob("./*") if file.is_file()] 58 | cmd = cmd + files 59 | 60 | p = Popen( 61 | ["python", "-m", "nbproject"] + cmd, 62 | stdout=PIPE, 63 | stderr=PIPE, 64 | cwd=nb_folder, 65 | ) 66 | ecode = p.wait() 67 | 68 | logger.debug(f"\n {cmd_name} exitcode: {ecode}.") 69 | logger.debug(p.stdout.read().decode()) 70 | logger.debug(p.stderr.read().decode()) 71 | 72 | if ecode != 0: 73 | raise Exception( 74 | f"Something happened with the cli command {cmd_name}, the exit code" 75 | f" is {ecode}." 76 | ) 77 | 78 | check_notebooks(nb_folder) 79 | -------------------------------------------------------------------------------- /tests/test_jupyter.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from nbproject.dev import _classic_nb_commands as _clsnbk 4 | from nbproject.dev import _jupyter_lab_commands as _juplab 5 | from nbproject.dev._frontend_commands import _reload_notebook, _save_notebook 6 | from nbproject.dev._jupyter_communicate import ( 7 | notebook_path, 8 | prepare_url, 9 | query_server, 10 | running_servers, 11 | ) 12 | from pytest import raises 13 | 14 | os.environ["NBPRJ_TEST_NBENV"] = "test" 15 | 16 | 17 | def test_jupyter_not_running(): 18 | assert notebook_path() is None 19 | assert notebook_path(return_env=True) is None 20 | 21 | servers_nbapp, servers_juserv = running_servers() 22 | assert list(servers_nbapp) == [] 23 | assert list(servers_juserv) == [] 24 | 25 | server = {"token": "test", "url": "localhost/"} 26 | 27 | assert ( 28 | prepare_url(server, "/test_query") 29 | == "localhost/api/sessions/test_query?token=test" 30 | ) 31 | 32 | with raises(Exception) as e_info: 33 | query_server(server) 34 | 35 | assert ( 36 | e_info.value.args[0] 37 | == "Unable to access server;\nquerying requires either no security or token" 38 | " based security." 39 | ) 40 | 41 | 42 | def test_juplab_clsnbk_nothing_happens(): 43 | _juplab._save_notebook() 44 | _juplab._reload_notebook() 45 | 46 | _clsnbk._save_notebook() 47 | _clsnbk._reload_notebook() 48 | 49 | for env in ("lab", "notebook"): 50 | _save_notebook(env) 51 | _reload_notebook(env) 52 | 53 | with raises(ValueError): 54 | _save_notebook("nonexistent_env") 55 | 56 | with raises(ValueError): 57 | _reload_notebook("nonexistent_env") 58 | -------------------------------------------------------------------------------- /tests/test_nbconvert.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from pathlib import Path 3 | 4 | 5 | def test_running_via_nbconvert(): 6 | result = subprocess.run( 7 | "jupyter nbconvert --to notebook --execute ./tests/for-nbconvert.ipynb", 8 | shell=True, 9 | capture_output=True, 10 | ) 11 | assert result.returncode == 1 12 | assert ( 13 | "Please execute notebook 'nbconvert' by passing option '--inplace'." 14 | in result.stderr.decode() 15 | ) 16 | 17 | result = subprocess.run( 18 | "jupyter nbconvert --to notebook --inplace --execute ./tests/for-nbconvert.ipynb", 19 | shell=True, 20 | capture_output=True, 21 | ) 22 | print(result.stdout.decode()) 23 | print(result.stderr.decode()) 24 | assert result.returncode == 0 25 | -------------------------------------------------------------------------------- /tests/test_notebooks.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import nbproject_test as test 4 | from nbproject._logger import logger 5 | 6 | 7 | def test_notebooks(): 8 | docs_folder = Path(__file__).parents[1] / "docs/" 9 | 10 | for check_folder in docs_folder.glob("./**"): 11 | logger.debug(f"\n{check_folder}") 12 | test.execute_notebooks(check_folder, write=True) 13 | -------------------------------------------------------------------------------- /tests/test_set_version.py: -------------------------------------------------------------------------------- 1 | from nbproject.dev._set_version import set_version 2 | 3 | 4 | def test_set_version(): 5 | # all remaining lines are covered in notebooks 6 | assert set_version(None, "1.2") == "1.2" 7 | assert set_version(None, "0") == "0" 8 | assert set_version(None, "1") == "1" 9 | assert set_version("1.2.3", "0") == "1.2.3" 10 | --------------------------------------------------------------------------------