├── .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 | [](https://github.com/laminlabs/nbproject)
2 | [](https://codecov.io/gh/laminlabs/nbproject)
3 | [](https://pypi.org/project/nbproject)
4 | [](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: 
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. |
|
"
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 | ""
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"{row.pop(0)} | "
20 | for col in row:
21 | html += f"{col} | "
22 | html += "
"
23 | html += "
"
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 |
--------------------------------------------------------------------------------