nbsync 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nbsync-0.1.0 → nbsync-0.1.1}/PKG-INFO +1 -1
- nbsync-0.1.1/docs/getting-started/configuration.md +35 -0
- {nbsync-0.1.0/docs-gen → nbsync-0.1.1/docs}/getting-started/first-steps.md +57 -62
- nbsync-0.1.1/docs/getting-started/installation.md +35 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/docs/index.md +35 -17
- {nbsync-0.1.0 → nbsync-0.1.1}/mkdocs.yaml +4 -21
- nbsync-0.1.1/notebooks/analysis.ipynb +69 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/pyproject.toml +2 -1
- {nbsync-0.1.0 → nbsync-0.1.1}/scripts/plot.py +1 -1
- nbsync-0.1.1/scripts/plotting.py +19 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/cell.py +28 -5
- nbsync-0.1.1/tests/__init__.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_cell.py +5 -5
- nbsync-0.1.0/docs-gen/examples/advanced.md +0 -256
- nbsync-0.1.0/docs-gen/examples/basic.md +0 -151
- nbsync-0.1.0/docs-gen/examples/tables.md +0 -143
- nbsync-0.1.0/docs-gen/features/dynamic.md +0 -127
- nbsync-0.1.0/docs-gen/features/images.md +0 -112
- nbsync-0.1.0/docs-gen/features/markdown.md +0 -134
- nbsync-0.1.0/docs-gen/features/overview.md +0 -62
- nbsync-0.1.0/docs-gen/features/python.md +0 -118
- nbsync-0.1.0/docs-gen/getting-started/configuration.md +0 -151
- nbsync-0.1.0/docs-gen/getting-started/installation.md +0 -99
- nbsync-0.1.0/docs-gen/usage/execution.md +0 -211
- nbsync-0.1.0/docs-gen/usage/markdown-files.md +0 -203
- nbsync-0.1.0/docs-gen/usage/notebook.md +0 -162
- nbsync-0.1.0/docs-gen/usage/python-files.md +0 -172
- nbsync-0.1.0/docs-gen/usage/tabbed-display.md +0 -247
- nbsync-0.1.0/docs-old/examples/format.md +0 -35
- nbsync-0.1.0/docs-old/examples/html.md +0 -15
- nbsync-0.1.0/docs-old/index.md +0 -122
- nbsync-0.1.0/docs-old/usage/class.md +0 -147
- nbsync-0.1.0/docs-old/usage/execute.md +0 -100
- nbsync-0.1.0/docs-old/usage/notebook.md +0 -159
- nbsync-0.1.0/notebooks/class.ipynb +0 -70
- nbsync-0.1.0/notebooks/format/html.ipynb +0 -136
- nbsync-0.1.0/notebooks/format/pdf.ipynb +0 -71
- nbsync-0.1.0/notebooks/format/png.ipynb +0 -59
- nbsync-0.1.0/notebooks/format/svg.ipynb +0 -442
- nbsync-0.1.0/notebooks/image.ipynb +0 -83
- nbsync-0.1.0/notebooks/index.ipynb +0 -59
- {nbsync-0.1.0 → nbsync-0.1.1}/.devcontainer/devcontainer.json +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/.devcontainer/postCreate.sh +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/.devcontainer/starship.toml +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/.gitattributes +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/.github/workflows/ci.yaml +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/.github/workflows/docs.yaml +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/.github/workflows/publish.yaml +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/.gitignore +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/LICENSE +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/README.md +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/scripts/__init__.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/__init__.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/logger.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/markdown.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/notebook.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/plugin.py +0 -0
- /nbsync-0.1.0/tests/__init__.py → /nbsync-0.1.1/src/nbsync/py.typed +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/sync.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/tests/conftest.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_markdown.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_notebook.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_plugin.py +0 -0
- {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_sync.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: nbsync
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: MkDocs plugin treating Jupyter notebooks, Python scripts and Markdown files as first-class citizens for documentation with dynamic execution and real-time synchronization
|
5
5
|
Project-URL: Documentation, https://daizutabi.github.io/nbsync/
|
6
6
|
Project-URL: Source, https://github.com/daizutabi/nbsync
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# Configuration
|
2
|
+
|
3
|
+
Configuring nbsync for your MkDocs site is simple but powerful, allowing you to
|
4
|
+
customize how notebooks and Python files are integrated with your documentation.
|
5
|
+
|
6
|
+
## Basic Configuration
|
7
|
+
|
8
|
+
To use nbsync with MkDocs, add it to your `mkdocs.yml` file:
|
9
|
+
|
10
|
+
```yaml
|
11
|
+
plugins:
|
12
|
+
- search
|
13
|
+
- nbsync
|
14
|
+
```
|
15
|
+
|
16
|
+
This minimal configuration uses all the default settings.
|
17
|
+
|
18
|
+
## Source Directory Configuration
|
19
|
+
|
20
|
+
Specify where nbsync should look for notebooks and Python files:
|
21
|
+
|
22
|
+
```yaml
|
23
|
+
plugins:
|
24
|
+
- search
|
25
|
+
- nbsync:
|
26
|
+
src_dir:
|
27
|
+
- ../notebooks # Path to notebooks directory
|
28
|
+
- ../scripts # Path to Python scripts
|
29
|
+
```
|
30
|
+
|
31
|
+
The `src_dir` option can be:
|
32
|
+
|
33
|
+
- A single path as a string
|
34
|
+
- A list of paths
|
35
|
+
- Relative to your docs directory
|
@@ -28,34 +28,24 @@ Update your `mkdocs.yml` to include nbsync:
|
|
28
28
|
```yaml
|
29
29
|
site_name: My Documentation
|
30
30
|
theme:
|
31
|
-
|
31
|
+
name: material
|
32
32
|
|
33
33
|
plugins:
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
34
|
+
- search
|
35
|
+
- nbsync:
|
36
|
+
src_dir:
|
37
|
+
- ../notebooks
|
38
|
+
- ../scripts
|
39
39
|
```
|
40
40
|
|
41
41
|
## Creating Your First Integration
|
42
42
|
|
43
43
|
### 1. Prepare a Jupyter Notebook
|
44
44
|
|
45
|
-
Create or use an existing notebook with visualizations.
|
46
|
-
reference with a comment:
|
45
|
+
Create or use an existing notebook with visualizations.
|
46
|
+
Tag cells you want to reference with a comment:
|
47
47
|
|
48
|
-
|
49
|
-
# In your notebook
|
50
|
-
# #simple-plot
|
51
|
-
import matplotlib.pyplot as plt
|
52
|
-
import numpy as np
|
53
|
-
|
54
|
-
x = np.linspace(0, 10, 100)
|
55
|
-
plt.figure(figsize=(8, 4))
|
56
|
-
plt.plot(x, np.sin(x))
|
57
|
-
plt.title("Simple Sine Wave")
|
58
|
-
```
|
48
|
+
{#simple-plot source="only" identifier="1" title="notebooks/analysis.ipynb"}
|
59
49
|
|
60
50
|
### 2. Reference in Your Documentation
|
61
51
|
|
@@ -66,32 +56,17 @@ In one of your markdown files (e.g., `docs/index.md`), add:
|
|
66
56
|
|
67
57
|
Here's a visualization from our analysis:
|
68
58
|
|
69
|
-
{#simple-plot}
|
70
60
|
```
|
71
61
|
|
62
|
+
{#simple-plot}
|
63
|
+
|
72
64
|
### 3. Create a Python Script
|
73
65
|
|
74
66
|
Create a file `scripts/plotting.py` with visualization functions:
|
75
67
|
|
76
|
-
```python
|
77
|
-
|
78
|
-
import matplotlib.pyplot as plt
|
79
|
-
import numpy as np
|
80
|
-
|
81
|
-
def plot_sine(frequency=1):
|
82
|
-
"""Plot a sine wave with given frequency."""
|
83
|
-
x = np.linspace(0, 10, 100)
|
84
|
-
plt.figure(figsize=(6, 3))
|
85
|
-
plt.plot(x, np.sin(frequency * x))
|
86
|
-
plt.title(f"Sine Wave (f={frequency})")
|
87
|
-
plt.ylim(-1.2, 1.2)
|
88
|
-
|
89
|
-
def plot_histogram(bins=20):
|
90
|
-
"""Plot a histogram of random data."""
|
91
|
-
data = np.random.randn(1000)
|
92
|
-
plt.figure(figsize=(6, 3))
|
93
|
-
plt.hist(data, bins=bins)
|
94
|
-
plt.title(f"Histogram (bins={bins})")
|
68
|
+
```python title="scripts/plotting.py"
|
69
|
+
--8<-- "scripts/plotting.py"
|
95
70
|
```
|
96
71
|
|
97
72
|
### 4. Use Functions in Your Documentation
|
@@ -103,7 +78,7 @@ Create a new file `docs/examples.md`:
|
|
103
78
|
|
104
79
|
Let's demonstrate different plots:
|
105
80
|
|
106
|
-
{#.}
|
107
82
|
|
108
83
|
## Sine Waves
|
109
84
|
|
@@ -118,6 +93,16 @@ Let's demonstrate different plots:
|
|
118
93
|
| ![](){`plot_histogram(20)`} | ![](){`plot_histogram(50)`} |
|
119
94
|
```
|
120
95
|
|
96
|
+
{#.}
|
97
|
+
|
98
|
+
| Frequency = 1 | Frequency = 2 |
|
99
|
+
| :-------------------: | :-------------------: |
|
100
|
+
| ![](){`plot_sine(1)`} | ![](){`plot_sine(2)`} |
|
101
|
+
|
102
|
+
| 20 Bins | 50 Bins |
|
103
|
+
| :-------------------------: | :-------------------------: |
|
104
|
+
| ![](){`plot_histogram(20)`} | ![](){`plot_histogram(50)`} |
|
105
|
+
|
121
106
|
### 5. Create a Markdown-Based Notebook
|
122
107
|
|
123
108
|
Create a file `docs/custom.md`:
|
@@ -127,8 +112,7 @@ Create a file `docs/custom.md`:
|
|
127
112
|
|
128
113
|
Here's an analysis created directly in markdown:
|
129
114
|
|
130
|
-
|
131
|
-
```python .md#data
|
115
|
+
```python .md#_
|
132
116
|
import numpy as np
|
133
117
|
import pandas as pd
|
134
118
|
|
@@ -139,35 +123,50 @@ data = pd.DataFrame({
|
|
139
123
|
'group': np.random.choice(['A', 'B', 'C'], 100)
|
140
124
|
})
|
141
125
|
```
|
142
|
-
````
|
143
|
-
````
|
144
126
|
|
145
127
|
```python .md#scatter
|
146
128
|
import matplotlib.pyplot as plt
|
147
129
|
import seaborn as sns
|
148
130
|
|
149
|
-
plt.figure(figsize=(
|
131
|
+
plt.figure(figsize=(3, 2))
|
150
132
|
sns.scatterplot(data=data, x='x', y='y', hue='group')
|
151
133
|
plt.title('Scatter Plot by Group')
|
152
134
|
```
|
153
135
|
|
154
|
-
![Scatter plot](){#scatter}
|
136
|
+
{#scatter}
|
137
|
+
````
|
155
138
|
|
139
|
+
```python .md#_
|
140
|
+
import numpy as np
|
141
|
+
import pandas as pd
|
142
|
+
|
143
|
+
# Generate sample data
|
144
|
+
data = pd.DataFrame({
|
145
|
+
'x': np.random.randn(100),
|
146
|
+
'y': np.random.randn(100),
|
147
|
+
'group': np.random.choice(['A', 'B', 'C'], 100)
|
148
|
+
})
|
156
149
|
```
|
157
150
|
|
151
|
+
```python .md#scatter
|
152
|
+
import matplotlib.pyplot as plt
|
153
|
+
import seaborn as sns
|
154
|
+
|
155
|
+
plt.figure(figsize=(3, 2))
|
156
|
+
sns.scatterplot(data=data, x='x', y='y', hue='group')
|
157
|
+
plt.title('Scatter Plot by Group')
|
158
158
|
```
|
159
159
|
|
160
|
+
![Scatter plot](){#scatter}
|
161
|
+
|
160
162
|
## 6. Run Your Documentation
|
161
163
|
|
162
164
|
Start the MkDocs development server:
|
163
165
|
|
164
166
|
```bash
|
165
|
-
mkdocs serve
|
167
|
+
mkdocs serve --open
|
166
168
|
```
|
167
169
|
|
168
|
-
Navigate to http://localhost:8000 to see your documentation with the integrated
|
169
|
-
visualizations.
|
170
|
-
|
171
170
|
## Next Steps
|
172
171
|
|
173
172
|
Now that you have the basics working, you can:
|
@@ -175,25 +174,21 @@ Now that you have the basics working, you can:
|
|
175
174
|
1. [Explore advanced notebook features](../usage/notebook.md)
|
176
175
|
2. [Learn about Python file integration](../usage/python-files.md)
|
177
176
|
3. [Discover markdown-based notebooks](../usage/markdown-files.md)
|
178
|
-
4. [See real-world examples](../examples/basic.md)
|
179
177
|
|
180
178
|
## Troubleshooting
|
181
179
|
|
182
180
|
### Common Issues
|
183
181
|
|
184
182
|
1. **Images Not Showing**:
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
- Verify Python dependencies are installed
|
183
|
+
- Check paths in your configuration
|
184
|
+
- Ensure notebooks have correctly tagged cells
|
185
|
+
- Verify Python dependencies are installed
|
189
186
|
|
190
187
|
2. **Execution Errors**:
|
191
|
-
|
192
|
-
|
193
|
-
- Ensure your environment has all required packages
|
194
|
-
- Increase timeout if operations are complex
|
188
|
+
- Check the console output for error messages
|
189
|
+
- Ensure your environment has all required packages
|
195
190
|
|
196
191
|
3. **Changes Not Reflecting**:
|
197
|
-
|
198
|
-
|
199
|
-
|
192
|
+
- Hard refresh your browser
|
193
|
+
- Restart the MkDocs server
|
194
|
+
- Check file paths and identifiers
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# Installation
|
2
|
+
|
3
|
+
Installing nbsync is straightforward and can be done using uv or pip,
|
4
|
+
the Python package manager.
|
5
|
+
|
6
|
+
## Prerequisites
|
7
|
+
|
8
|
+
Before installing nbsync, ensure you have the following:
|
9
|
+
|
10
|
+
- Python 3.10 or higher
|
11
|
+
- uv or pip (Python package manager)
|
12
|
+
- MkDocs 1.6 or higher (documentation generator)
|
13
|
+
|
14
|
+
## Basic Installation
|
15
|
+
|
16
|
+
Install nbsync using uv or pip:
|
17
|
+
|
18
|
+
```bash
|
19
|
+
uv pip install nbsync
|
20
|
+
# or
|
21
|
+
pip install nbsync
|
22
|
+
```
|
23
|
+
|
24
|
+
This command installs the latest stable version of nbsync and its core
|
25
|
+
dependencies.
|
26
|
+
|
27
|
+
## Installation of nbconvert
|
28
|
+
|
29
|
+
For dynamic execution functionality, install nbconvert:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
uv pip install nbconvert
|
33
|
+
# or
|
34
|
+
pip install nbconvert
|
35
|
+
```
|
@@ -23,18 +23,43 @@
|
|
23
23
|
|
24
24
|
## What is nbsync?
|
25
25
|
|
26
|
-
nbsync is an innovative plugin that
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
nbsync is an innovative MkDocs plugin that treats Jupyter notebooks,
|
27
|
+
Python scripts, and Markdown files as first-class citizens for
|
28
|
+
documentation. Unlike traditional approaches, nbsync provides equal
|
29
|
+
capabilities across all file formats, enabling seamless integration
|
30
|
+
and dynamic execution with real-time synchronization.
|
30
31
|
|
31
32
|
It solves common challenges faced by data scientists, researchers, and technical
|
32
33
|
writers:
|
33
34
|
|
34
35
|
- **Development happens in notebooks** - ideal for experimentation and visualization
|
35
36
|
- **Documentation lives in markdown** - perfect for narrative and explanation
|
37
|
+
- **Code resides in Python files** - organized and version-controlled
|
36
38
|
- **Traditional integration is challenging** - screenshots break, exports get outdated
|
37
39
|
|
40
|
+
## Inspiration & Comparison
|
41
|
+
|
42
|
+
nbsync was inspired by and builds upon the excellent work of two MkDocs
|
43
|
+
plugins:
|
44
|
+
|
45
|
+
- [**markdown-exec**](https://pawamoy.github.io/markdown-exec/) - Provides utilities to execute code blocks in Markdown files
|
46
|
+
- [**mkdocs-jupyter**](https://mkdocs-jupyter.danielfrg.com/) - Enables embedding Jupyter notebooks in MkDocs
|
47
|
+
|
48
|
+
While these plugins offer great functionality, nbsync takes a unified
|
49
|
+
approach by:
|
50
|
+
|
51
|
+
1. **Equal treatment** - Unlike other solutions that prioritize one format, nbsync treats Jupyter notebooks, Python scripts, and Markdown files equally as first-class citizens
|
52
|
+
2. **Real-time synchronization** - Changes to source files are immediately reflected in documentation
|
53
|
+
3. **Seamless integration** - Consistent syntax across all file formats allows for flexible documentation workflows
|
54
|
+
4. **Image syntax code execution** - Unique ability to execute code and embed visualizations anywhere Markdown image syntax (``) is valid, including tables, lists, and complex layouts
|
55
|
+
|
56
|
+
## Acknowledgements
|
57
|
+
|
58
|
+
The development of nbsync would not have been possible without the
|
59
|
+
groundwork laid by markdown-exec and mkdocs-jupyter. We extend our
|
60
|
+
sincere gratitude to the developers of these projects for their
|
61
|
+
innovative contributions to the documentation ecosystem.
|
62
|
+
|
38
63
|
## Key Features
|
39
64
|
|
40
65
|
### Notebooks from Markdown
|
@@ -44,20 +69,20 @@ documentation. Present code and its output results concisely with tabbed
|
|
44
69
|
display.
|
45
70
|
|
46
71
|
````markdown source="tabbed-nbsync"
|
47
|
-
```python .md#plot
|
72
|
+
```python .md#plot
|
48
73
|
import matplotlib.pyplot as plt
|
49
74
|
|
50
75
|
fig, ax = plt.subplots(figsize=(2, 1))
|
51
76
|
ax.plot([1, 3, 3, 4])
|
52
77
|
```
|
53
78
|
|
54
|
-
![Plot result](){#plot source="
|
79
|
+
![Plot result](){#plot source="above"}
|
55
80
|
````
|
56
81
|
|
57
82
|
### Python File Integration
|
58
83
|
|
59
|
-
Directly reference external Python files and reuse defined functions or
|
60
|
-
Avoid code duplication and improve maintainability.
|
84
|
+
Directly reference external Python files and reuse defined functions or
|
85
|
+
classes. Avoid code duplication and improve maintainability.
|
61
86
|
|
62
87
|
```python title="plot.py"
|
63
88
|
--8<-- "scripts/plot.py"
|
@@ -82,7 +107,8 @@ layouts.
|
|
82
107
|
### Dynamic Updates and Execution
|
83
108
|
|
84
109
|
Automatic synchronization between notebooks and documentation ensures code
|
85
|
-
changes are reflected in real-time. View changes instantly in MkDocs serve
|
110
|
+
changes are reflected in real-time. View changes instantly in MkDocs serve
|
111
|
+
mode.
|
86
112
|
|
87
113
|
## Getting Started
|
88
114
|
|
@@ -91,11 +117,3 @@ Follow these steps to get started with nbsync:
|
|
91
117
|
1. [Installation](getting-started/installation.md)
|
92
118
|
2. [Configuration](getting-started/configuration.md)
|
93
119
|
3. [First Steps](getting-started/first-steps.md)
|
94
|
-
|
95
|
-
## Examples
|
96
|
-
|
97
|
-
Explore the possibilities of nbsync through practical examples:
|
98
|
-
|
99
|
-
- [Basic Usage](examples/basic.md)
|
100
|
-
- [Visualizations in Tables](examples/tables.md)
|
101
|
-
- [Advanced Examples](examples/advanced.md)
|
@@ -39,7 +39,6 @@ theme:
|
|
39
39
|
- navigation.tracking
|
40
40
|
- search.highlight
|
41
41
|
- search.suggest
|
42
|
-
- toc.follow
|
43
42
|
plugins:
|
44
43
|
- search
|
45
44
|
- nbsync:
|
@@ -64,23 +63,7 @@ markdown_extensions:
|
|
64
63
|
permalink: true
|
65
64
|
nav:
|
66
65
|
- Home: index.md
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
# - features/images.md
|
72
|
-
# - features/dynamic.md
|
73
|
-
# - Getting Started:
|
74
|
-
# - getting-started/installation.md
|
75
|
-
# - getting-started/configuration.md
|
76
|
-
# - getting-started/first-steps.md
|
77
|
-
# - Usage:
|
78
|
-
# - usage/notebook.md
|
79
|
-
# - usage/python-files.md
|
80
|
-
# - usage/markdown-files.md
|
81
|
-
# - usage/execution.md
|
82
|
-
# - usage/tabbed-display.md
|
83
|
-
# - Examples:
|
84
|
-
# - examples/basic.md
|
85
|
-
# - examples/tables.md
|
86
|
-
# - examples/advanced.md
|
66
|
+
- Getting Started:
|
67
|
+
- getting-started/installation.md
|
68
|
+
- getting-started/configuration.md
|
69
|
+
- getting-started/first-steps.md
|
@@ -0,0 +1,69 @@
|
|
1
|
+
{
|
2
|
+
"cells": [
|
3
|
+
{
|
4
|
+
"cell_type": "code",
|
5
|
+
"execution_count": 5,
|
6
|
+
"metadata": {},
|
7
|
+
"outputs": [
|
8
|
+
{
|
9
|
+
"data": {
|
10
|
+
"text/plain": [
|
11
|
+
"Text(0.5, 1.0, 'Simple Sine Wave')"
|
12
|
+
]
|
13
|
+
},
|
14
|
+
"execution_count": 5,
|
15
|
+
"metadata": {},
|
16
|
+
"output_type": "execute_result"
|
17
|
+
},
|
18
|
+
{
|
19
|
+
"data": {
|
20
|
+
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAR8AAAC1CAYAAABmp/txAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvc2/+5QAAAAlwSFlzAAAPYQAAD2EBqD+naQAALAFJREFUeJztnXl4U8X6x78nSZN0S9rSfd8paymlLV0AxXorVmSRXaEsynIBRbw/Fb1SuV5ExatXuaiICgoqi7Ko7NYC0kJbWspaSoFuQHdo04UmbTK/P9JEQndIcrLM53nyPM05c86805PznZl3Zt5hCCEEFAqFomc4bBtAoVDMEyo+FAqFFaj4UCgUVqDiQ6FQWIGKD4VCYQUqPhQKhRWo+FAoFFag4kOhUFiBig+FQmEFKj4s4evri9mzZ7OS99tvvw2GYVjJm81yUwwLKj5a5vz585g0aRJ8fHwgFArh4eGBxx9/HOvWrWPbNJ1iyOXOzMwEwzD4+OOP250bN24cGIbBpk2b2p0bOXIkPDw89GGiWcLQtV3aIz09HY8++ii8vb2RlJQEV1dXlJaW4tSpU7h27RquXr2qTiuVSsHhcGBhYaF3O99++22sWrUK2nr0hl7u1tZWiMViPPHEE/j55581zjk5OaG2thZJSUn46quv1MdlMhnEYjHGjh2LHTt26M1Wc4LHtgGmxOrVqyEWi5GVlQU7OzuNc5WVlRrfBQKBHi3TLYZebh6Ph6ioKKSlpWkcz8/PR3V1NWbMmIETJ05onMvOzkZzczPi4uL0aapZQbtdWuTatWsYMGBAuxcQAJydnTW+3+/72Lx5MxiGwYkTJ/Diiy/CyckJdnZ2WLBgAWQyGWprazFr1izY29vD3t4er776qkbLpaioCAzD4MMPP8THH38MHx8fWFpaYtSoUbhw4UKP7N+6dSvCw8NhaWkJBwcHTJs2DaWlpXopd1paGpYvXw4nJydYW1tjwoQJqKqqane/AwcOYMSIEbC2toatrS0SExNx8eLFbm2Mi4tDRUWFRissLS0NIpEI8+fPVwvRvedU1wHA3r17kZiYCHd3dwgEAgQEBOCdd96BXC5XX7NkyRLY2NigqampXf7Tp0+Hq6urRvoHLYupQMVHi/j4+CA7O7vHL3tHLF26FAUFBVi1ahWefvppfPnll3jrrbcwduxYyOVyvPvuu4iLi8PatWuxZcuWdtd/9913+PTTT7F48WKsWLECFy5cwOjRo1FRUdFlvqtXr8asWbMQFBSEjz76CMuWLUNKSgpGjhyJ2tpavZT77NmzSE5OxqJFi/Drr79iyZIlGmm2bNmCxMRE2NjY4P3338dbb72FS5cuIS4uDkVFRV3eXyUi97Zw0tLSMHz4cERFRcHCwgLp6eka52xtbREaGgpAKZI2NjZYvnw5PvnkE4SHh2PlypV4/fXX1ddMnToVjY2N2Ldvn0beTU1N+PXXXzFp0iRwudyHLovJQCha4/Dhw4TL5RIul0uio6PJq6++Sg4dOkRkMlm7tD4+PiQpKUn9fdOmTQQASUhIIAqFQn08OjqaMAxDFi5cqD7W2tpKPD09yahRo9THCgsLCQBiaWlJbty4oT6ekZFBAJCXX35ZfSw5OZnc++iLiooIl8slq1ev1rDx/PnzhMfjtTuui3LHx8drlPvll18mXC6X1NbWEkIIqa+vJ3Z2duSFF17QuF95eTkRi8Xtjt+PRCIhXC6XzJs3T32sb9++ZNWqVYQQQiIjI8n//d//qc85OTmRxx9/XP29qamp3T0XLFhArKysSHNzMyGEEIVCQTw8PMgzzzyjkW7Hjh0EADl+/LhWymIq0JaPFnn88cdx8uRJPP300zh79iw++OADJCQkwMPDA7/88kuP7jFv3jyNYfCoqCgQQjBv3jz1MS6Xi2HDhuH69evtrh8/frzGCE1kZCSioqKwf//+TvPctWsXFAoFpkyZgurqavXH1dUVQUFBSE1N1Xm558+fr1HuESNGQC6Xo7i4GABw5MgR1NbWYvr06Ro2crlcREVFdWujra0tBg8erG75VFdXIz8/HzExMQCA2NhYdVfrypUrqKqq0vD3WFpaqv+ur69HdXU1RowYgaamJly+fBkAwDAMJk+ejP3796OhoUGdfvv27fDw8FDf72HLYipQ8dEyERER2LVrF+7cuYPMzEysWLEC9fX1mDRpEi5dutTt9d7e3hrfxWIxAMDLy6vd8Tt37rS7PigoqN2x4ODgLpvyBQUFIIQgKCgITk5OGp+8vLx2TuOO0Ha57e3tAUBdxoKCAgDA6NGj29l4+PDhHtkYFxen9u2kp6eDy+Vi+PDhAICYmBhkZ2dDKpW28/cAwMWLFzFhwgSIxWKIRCI4OTnhueeeAwDU1dWp002dOhV3795Vi25DQwP279+PyZMnq8VVG2UxBehol47g8/mIiIhAREQEgoODMWfOHOzcuRPJycldXqfyCfTkONHSULlCoQDDMDhw4ECH+djY2PT4Xtout6qMCoUCgNJX4urq2i4dj9f9TzkuLg7r1q1DWloa0tPTMWjQIHXZYmJiIJVKkZWVhRMnToDH46mFqba2FqNGjYJIJMK//vUvBAQEQCgUIicnB6+99praNgAYPnw4fH19sWPHDsyYMQO//vor7t69i6lTp6rTaKMspoB5lJJlhg0bBgAoKyvTeV6qWvVerly5Al9f306vCQgIACEEfn5+CA4O1pot2ix3QEAAAOXoWXx8/APd416n88mTJxEbG6s+5+7uDh8fH6SlpSEtLQ1hYWGwsrICABw9ehQ1NTXYtWsXRo4cqb6msLCww3ymTJmCTz75BBKJBNu3b4evr69ayLRVFlOAdru0SGpqaoetEZW/pW/fvjq3Yc+ePbh586b6e2ZmJjIyMjBmzJhOr5k4cSK4XG6HEw8JIaipqekyT32UOyEhASKRCO+++y5aWlrane9oWP5+3N3d4efnh5SUFJw+fVrt71ERExODPXv2ID8/X6PLpWqV3VtGmUyGzz77rMN8pk6dCqlUim+//RYHDx7ElClTtF4WU4C2fLTI0qVL0dTUhAkTJiAkJAQymQzp6enq2m/OnDk6tyEwMBBxcXFYtGgRpFIp/vvf/6JPnz549dVXO70mICAA//73v7FixQoUFRVh/PjxsLW1RWFhIXbv3o358+fjH//4R6fX66PcIpEIn3/+OWbOnImhQ4di2rRpcHJyQklJCfbt24fY2Fj873//6/Y+cXFx6ikK97Z8AKX4/Pjjj+p09x63t7dHUlISXnzxRTAMgy1btnTa7R06dCgCAwPx5ptvQiqVanS5tFkWo4elUTaT5MCBA2Tu3LkkJCSE2NjYED6fTwIDA8nSpUtJRUWFRtrOhpyzsrI00qmGxauqqjSOJyUlEWtra/V31VD72rVryX/+8x/i5eVFBAIBGTFiBDl79myH97yfn3/+mcTFxRFra2tibW1NQkJCyOLFi0l+fr7ey52amkoAkNTU1HbHExISiFgsJkKhkAQEBJDZs2eT06dPd2mjig0bNhAAxMPDo925nJwcAoAAaGd3WloaGT58OLG0tCTu7u7q6QQd2UgIIW+++SYBQAIDAzu15WHLYuzQtV0mQlFREfz8/LB27douWykUiqFAfT4UCoUVqPhQKBRWoOJDoVBYgfp8KBQKK9CWD4VCYQUqPhQKhRUMepKhQqHArVu3YGtry1rAcwqF0jmEENTX18Pd3R0cTu/aMgYtPrdu3Wq3mptCoRgepaWl8PT07NU1OhWf48ePY+3atcjOzkZZWRl2796N8ePH9/h6W1tbAMqCiUQiHVlJoVAeFIlEAi8vL/W72ht0Kj6NjY0IDQ3F3LlzMXHixF5fr+pqiUQiKj4UigHzIG4RnYrPmDFjulxNTaGYA4QQ6rPsAIPy+UilUkilUvV3iUTCojXd09wix/7zZTh+pQoZhbdxp0kGhQJwshUgwtceo/o6IXGQO/g8OqhoLtQ0SHHwYjkOX6zApTIJJHdboCAEgc62GOguwoShHoj270PFCHqcZMgwTLc+H9VmdvdTV1dnUN2uVrkC27JKsT71KsrqmrtM62FniUWPBGB6pDe4HPqDM1WaZK348vh1bDh2HXdb5F2mDXG1xWtjQvBoX+cu0xkDEokEYrH4gd5RgxKfjlo+Xl5eBiU+5XXNWPpjDrKKlLGFXUVCTAr3RExgH3jZW4HDYVBY1YiMwhpsyypFVb2yPDEBffDx1CFwEQnZNJ+iA86W1uLv3+fgZu1dAEB/NxGeCnVDbIAj+tjwQQhwubwex65U4ufsm2pxmhbhhX8+1R82AoPqgPQKkxGf+3mYgumCjOs1WPR9Dm43ymAj4OGVvwVjeqQ3hBYdxx9ubpHjh4wSrD2Uj7stcjhY87FxVjjCfRz0bDlFV/yUfQNv7D4PWasCHnaWWPFkCBIHuXXaraprasEnKQX4Jk0ZgjXYxQbfzY2Cq9g4K6WHeUepM6KHpF2tRtKmTNxulKG/mwi/Lo3DnFi/ToUHAIQWXMyN88NvL8ZhgLsItxtlmPl1Jk5e6zosKcU4+PpEIf6x8yxkrQrE93PGgWUj8NRg9y79OWIrC6wc2x8/vjAczrYCXKlowDOfp+N6VUOn15gqOhWfhoYG5ObmIjc3F4Ay4HZubi5KSkp0ma3WOVFQjbmbs9DcosCjfZ2w6+8x8HO07vH1AU42+GlhDEYEOaJJJsfsTZlIv1bd/YUUg2XrqWK885tyS6CFowLw5cxhEAktenx9dEAf/LxI+Tu6WXsX0748pe62mQs6FZ/Tp08jLCwMYWFhAIDly5cjLCwMK1eu1GW2WuVKRT0Wbs2GtK12+2JmeJetnc6w5HOxcdYwPBbiDGmrAgu3ZJtlbWcK7DtXhn/uUW4NvXBUAF57oi84DzCY4OVghZ0LoxHsYoPKeinmbsqCpLl9QHlTRafi88gjj4AQ0u6zefNmXWarNe40yvD8t6fRIG1FpJ8DPns2HAJe74VHhdCCi/XPDsVQbztImlsx79vTqG2SadFiiq7JL6/H//10FgCQFO2D157o+1DD5o42AmyaEwlnWwHyK+rx9605kCvMI8oN9fl0gkJB8OK2Myi53QRPe0t88Vy4VubrCC242DBzGDzsLFFY3YiXt+dqbfM/im6pa2rB/C2n0SSTIy7QEW891V8r83U87CzxzewIWPG5OHG1Guv+aL/3milCxacTvj5RiD8LqmFpwcXXSRFwsOZr7d5OtgJ8lTQMAh4HqflV+Da9SGv3puiON/acR3GNsjJaNz0MPK72Xp+BHmKsnjAQAPBpSgFOXTf9QQkqPh2QVybB2kP5AICVY/ujr2vvF811Rz83Ed54sh8A4N0Dl5FfXq/1PCja45ezt7DvXBl4HAafPTsU9lqsjFRMCPPEpHBPKAiwbFsu6ppM2/9Dxec+ZK0KLNuWC5lcgfh+LpgWobuQHrOiffBoXyfIWhV4eXsuWuWK7i+i6J1KSTPeanMwLxkdiMGedjrLa9XTA+DvaI1ySTPWHMjTWT6GABWf+9j453XkV9SjjzUf7z0zSKdrcBiGwQeTQiG2tMClMgk20+6XQfLPPRdQd7cFgzzEWPxooE7zshbw8P6kwQCAbVmlJj0lg4rPPZTeblI7+95M7AdHG4HO83SyFWDFmBAAwH8OXzG7uR6GTmp+JQ5fqgCXw+DDyaGw0KKfpzMifB3wbJQ3AOCNXefR3M1aMWOFik8bhBAk/3IRzS0KDPd3wIQwD73lPWWYFyJ87XG3RY7kvRf1li+la5pb5Hj7F+XzmBvrqxPfX2e8NiYELiIBimqasPH4db3lq0+o+LRxNL8Kf1yuhAWXwb/H67a7dT8cDoN3JwwCj8Pg97wKpF013aa2MbHx+HUU1zTBRSTAS/HBes1bJLTAm4n9AQCfHb2GCknX0ROMESo+UIbIeHe/0rk3J9YPgc42erchyMUWzw33AQD8e1+e2Uw0M1QqJc34/Ng1AMAbT/ZjZeX52MFuGOpth7stcnxwMF/v+esaKj4AdmbfQEFlA+ysLHTuUOyKlx4LgkjIQ16ZBD9n32DNDgrw35QCNMnkGOJlh6dD3VmxgWEYrBw7AADwc84NnLtRy4odusLsxadR2or/HL4CAHhxdBDElj1fHKht7K35WDo6CADw4eF83JWZpqPR0LlW1YDtWaUAlK0eNqMODvGyw8Q2/6Nq7pmpYPbiszm9CNUNUvj0sVJ3e9hkVowPPO0tUVkvxZZTRWybY5Z8cPAy5AqC+H7OiPRjP/bSy48Hw4LL4M+CapMKx2LW4iNpbsGXbSMJyx8PNohYywIeFy8+pmz9fHHsOhqkrSxbZF5cuFmHQxcrwGGA154IYdscAMrV79MilEPvHx7ON5m1gOy/bSzyzYlC1N1tQZCzDZ4azE6/viMmhnnAz9EatxtldN2Xnvnv78p5XuOGeCDIRX9D692xZHQgBDwOsovvIDW/km1ztILZik9dUwu+/lMZynJZfLBBBXfncTl4qa31s+HYNbOK8cImF27W4fc8ZatnyWj2Bh46wkUkRFKMLwDgk5SrJtH6MVvx+SatEPXSVoS42mLMQFe2zWnH2FB3BDrbQNLcii0ni9k2xyy4t9UT4KT/6RbdMX+kP4QWHJwtrcUJE5gLZpbi0yBtVa+jWjI68IGi0OkaLofB4kcDACi7h3TkS7dcuiUx2FaPCkcbAaZHKn0/6/64yrI1D49Zis+PGSWou9sCf0drjBnoxrY5nTJ2sDs87S1R0yjD9izjinttbHzRNqEwcbC7QbZ6VMwf6Q8+l4PMwtvIMPKYP2YnPtJWOb46oRzhWjDK36B8PffD43KwcJSy9bPh+HXIWmnIDV1QUtOE387dAgAsGOnPsjVd4ya2xKRhngCUyy6MGbMTn105N1EhkcJNLMSEME+2zemWSeGecLYVoKyuGXtzb7Jtjkmy8c/rUBBgZLATBnqI2TanWxaM9AeHAY5dqcLlcsPeUrwrzEp8FAqCjX8qWz3z4vwMYl5PdwgtuJgT6wdAGdrVFEY5DInqBil2nFbOZl7U1so0dHz6WOOJtkGSjccLWbbmwTH8t0+LpOZX4npVI2yFPExrc9wZAzOivGHN5+JyeT3+LDD+UQ5DYuupYkhbFQj1ssNwf/ZnM/eUF0You4e/nL2J8jrjXPFuVuKjavXMiPQ2qv2xxZYWmNIWzlVVBsrD09wiV09jeD7Oj9U1XL0lzNsekb4OaJETbEozztaP2YjPhZt1OHX9NngcRj1Zy5iYG+sHDgP8WVCNvDLj7ecbEr/k3kJNowwedpYGOderO+a3Ocd/yCxBoxEuwzEb8fmqrcWQONgN7naWLFvTe7wcrNTTAoy1pjMkCCHqUc+kGB+tboOjL0aHOMO3jxXqm1uxK8f4QrAY33/8AaiUNGPf+TIASkezsTI3zhcAsCf3Fm430p1OH4a0qzW4UtEAKz4XUyOMx/93LxwOg9ltrfhNaUVQGFkAOrMQn+8zStAiJwj3sdfptie6Zqi3PQZ5iCFrVeDHTDrp8GHYnK5sPU4O92Q1htPDMmmYF2wFPFyvbsSxgiq2zekVJi8+slYFvs9QvqizjdDXcy8Mw2BOrC8AYMvJYrTQfb4eiJKaJqRcVq4MN0b/373YCHiYPEw5GLEprYhdY3qJyYvP/vNlqG6QwkUkUM+NMGYSB7vB0YaPckkzDl4oZ9sco+S7k0UgbZMK/Q14KUVPmR3jC4YBjl+pwvWqBrbN6TEmLz6b2haQPhflo5c9l3SNgMfFjChlxMXvThaxa4wR0iRrVU8qnB3DfuRKbeDdxwqj+zoDALacMp4ICMb/NnbBuRu1OFtaCz6Xg+lRxulU7Ihno7zB4zDIKrpDh917yZ4ztyBpboVPHys8EuzMtjlaY2a0Ukh/yr6BJplxDLubtPioJpA9OchVL7uP6gsXkRAJA5RdSGOq6diGEKJuLc4c7mOQoVQelJFBTuph9z1nbrFtTo8wWfGpbZLhl7PKh6CqFUwJVbD7PWdu0kiHPSSn5A4ul9dDaMHB5HAvts3RKhwOo/5NKH1ahj/sbrLis/P0DUhbFejvJsJQb3u2zdE6w/0dEOxigyaZnO7x1UNULeGnQ90htjLe4fXOmBzuBUsL5RrA08V32DanW0xSfBQKgq0Zyh/azGgfo1qz01MYhsHMtppuy6lio6jp2KS6QYr955WjgzOH+7JrjI4QW1moNzjcagTdcZMUn7Rr1SiuaYKtgIdxQwxnVwptMz7MA1Z8Lq5XNeLU9dtsm2PQbM8qhUyuXL0+yNPwY/Y8KKqu14Hz5ahpkLJsTdeYpPioVP+ZcE9Y8Y1n9XpvsRVaYHzbbpbfZxh+TccWcgVRzwh/1oRGPTtikKcYoZ5iyOQK7Dht2N1xkxOf8rpm/J6nnL06w8R/aMBfL9Ohi+Woqjfsmo4tjhdU4caduxAJeRhrQPuz6Ypn2+aB/ZBZbNDrvUxOfLZnlUKuIIj0dUCwAW36pisGuIsR5m2HFjlRT56jaPL9KWWr55lwT1jyuSxbo3vGhrpDJOSh9PZdHDfg9V4mJT6tcgW2te3y8Oxw02/1qFDXdBklkBtwTccGZXV38cflCgCm3+VSYcnnYuJQZXzyHzIMdwGySYlPan4Vyuqa4WDNN4l1XD3lqcFuEFta4GatYdd0bLAtsxQKAkT5OSDQ2fRbwipUQptyudJgw6yalPj80OZ0nRzuCQHP9JvXKoQWXEwcqnQ8G3JNp2/ubQmbg//vXoJcbBHp6wC5gmB7lmF2x/UiPuvXr4evry+EQiGioqKQmZmp9TxKbzfh6BVlrT/diILDawtVTffH5UqU1d1l2RrD4I/LlaiQSM2uJaxC5XrYnmWY3XGdi8/27duxfPlyJCcnIycnB6GhoUhISEBlZaV288kqBSFAbGAf+Dpaa/XexkCgs+HXdPrmh7bhdXNrCat4YqAr7K0scKuuGUfztfu+aQOdi89HH32EF154AXPmzEH//v3xxRdfwMrKCt98843W8miRK7C9baRnRqTprePqKX/VdKVoNfNAY6W3m3DMjFvCgDL8yqRww3U861R8ZDIZsrOzER8f/1eGHA7i4+Nx8uTJdumlUikkEonGpyf8fqkCVfVSONoI8Hh/F63Zb2yoarqyumYczTdvx7OqJRwX6GiWLWEVKuFNza/EzVrD6o7rVHyqq6shl8vh4qIpCC4uLigvbx+Fb82aNRCLxeqPl1fPVh4P9BBj/kh/PD/COHYh1RUaNZ0Zx3jWaAmbmaP5fvydbBDt3wcKAmw3sN+EQb2pK1asQF1dnfpTWtoz34WXgxXeeLIfFhrJdre6RFXTHTXAmk5f3NsSju9nvi1hFSoB3n7asLrjOhUfR0dHcLlcVFRUaByvqKiAq2v70QeBQACRSKTxofQOQ67p9IWq1Tc1wtOsW8IqEga4oo81HxUSqTpwviGg0yfD5/MRHh6OlJQU9TGFQoGUlBRER0frMmuzRuV43pZVanY7XBRVN+LPgmowDDDNSPfj0jZ8HgeThhme41nn1cLy5cuxceNGfPvtt8jLy8OiRYvQ2NiIOXPm6Dprs+Vv/V3haMNHZb0UKXmGU9Ppgx/bJhWODHKCl4MVy9YYDjPauuPHC6pQUtPEsjVKdC4+U6dOxYcffoiVK1diyJAhyM3NxcGDB9s5oSnag8/jqPdyMqdQG9JWOX5qCyNhLuu4eopPH2uMCHIEIX8JNNvopUO8ZMkSFBcXQyqVIiMjA1FRUfrI1qyZHuENhgH+LKhGcU0j2+bohYMXylHTKIOrSIjRIaazM4W2UAnyztOlkLWy3x2n3jgTxbuPFUYGOQEwn2F3VeiMaZFe4JnAHm3a5rF+LnC2FaC6QYbDl9jfcJI+IRPmr5ruBqStcpat0S1XKuqRWXQbXA5DHc2dYMHlYFqEsjuuCqbPJlR8TJjRIc5wEwtxu1GGA+fZr+l0yfdtoXPj+znDVSxk2RrDZVqkNzgMkFF4GwUV9azaQsXHhOFxOepJh8awm8GD0iRrxa6cmwD+CqBO6Rh3O0v1xMvvWR52p+Jj4kyL8AKPw+B08R1cumWaWyvvOXML9VLlFsixAY5sm2PwqDbR/Dn7Bhql7G2tTMXHxHEWCZEw0HS3VjblLZB1RWyAI/wcrVEvbcXeXPa2VqbiYwbMNOGtlU8Xm+4WyLqCw2HUgxFsbq1MxccMiPJzQJCzDe62mN7Wyt+1jdqMC/UwyS2QdcXkcC8ILTi4XF6PrCJ2tlam4mMGMAyDWW39/O9OGvZeTr2hsr4ZBy+UAfjLj0HpGWIrC0xo23Dy2/QiVmyg4mMmTBzqCVsBD4XVjThmIjtc/JBRghY5wVBvOwz0MN0tkHXFrGhfAMDBi+Ws7HBBxcdMsBbw1Ou92KrptIm0VY6tbTOaZ8f6sWyNcdLPTYRIP2XcbzbWAFLxMSNmRfuAYYCj+VW4XtXAtjkPxb5zZahukMJVJMQYM9yZQlvMjvEFoGxFNrfodxY8FR8zwtfRGo/2VS64NObWDyEEm9KKACh9PRZ0HdcD87f+LnAXC1HTKMMveh52p0/NzJgT6wsA2Jl9A3VNxjnsnlNyB+dv1kHA45jtzhTagsflIKmt9fNNWqFeh92p+JgZcYGOCHG1RZNMbjBxXXrLV38WAgDGD/GAgzWfZWuMn2mR3rDic3G5vB5pV2v0li8VHzODYRjMi1M6aDenFRldmNXimkYcvKhcJPv8COpo1gZiSwtMbtv15OsT1/WWLxUfM+TpIe5wshWgXNKMfefK2DanV3x9ohCEAI/2dUKQiy3b5pgMc2L9wDBAan4VruhptTsVHzNEwOMiqW1S3obj11mbXt9b7jTKsKNtP64XRvqzbI1p4etojYT+ylHDDcf00/qh4mOmPDfcB1Z8LvLKJOpthQ2d704Wo7lFgYEeIkT792HbHJNj4SPKfe/25t7ELT3s+UbFx0yxs+KrdzT47Og1lq3pnkZpKzalKx3NL4zwB8PQ1evaZoiXHYb7O6BVQfD1iUKd50fFx4yZN8IPFlwGmYW3kV18m21zuuSHjBLUNrXAt48VnhrszrY5Jotq198fM0tQ2yTTaV5UfMwYN7ElJoYpRznWpxpu66e5RY4v/1T6IRY9EgAujdmjM0YFO6GfmwiyVgUyCnVbIfF0eneKwbNglD92Zpfij8uVOHejFoM97dg2qR0/Zd9AVb0U7mIhJrSJJUU3MAyD958ZhD42AnjYWeo0L9ryMXP8nWwwfogytMJ/fy9g2Zr2NLfIsT71KgBg/kh/uve6Hhjsaadz4QGo+FAALBkdCA4D/HG5ErmltWybo8G2zBKU1TXDVSTENLqUwqSg4kNRtn7CVK2fKyxb8xdNslb8r80XtfSxQAgtuCxbRNEmVHwoAIAXRweBy2FwNL8KJ6/pb31PV3x3shjVDVJ4OVjS+MwmCBUfCgDlDNfpkcoXfM2BPNZDrd5plOGzNl/Pi6ODqK/HBKFPlKLmpceCYc3n4tyNOvx2nt01X5+kFEDS3IoQV1tMHEpHuEwRKj4UNU62AvUksw8OXtZ7ZDsVVysb1HuM/TOxP53XY6JQ8aFoMG+EH1xEAty4cxdfHGNn4uG7+/MgVxA8FuKMuCC6A6mpQsWHooEVn4e3nuoPQLnmq7imUa/5H7xQjj8uV4LHYfBGYj+95k3RL1R8KO1IHOSGEUGOkLUqsHLvRb2F3KhvbkHyLxcAKGdeBzjZ6CVfCjtQ8aG0g2EYrHp6APhcDo5dqcKe3Jt6yXftoXxUSKTw7WOFpaOD9JInhT2o+FA6xN/JBi8+FggAWLn3os7ju6RfrVY7mVdPGEQnFJoBVHwonbJwVADCvO1Q39yKf+w8q7O5P7cbZVi2PReEANMjvRAbSJ3M5gAVH0qn8LgcfDRlCCwtuEi/VoPPjl7Veh6EELz60zlU1ksR4GStdnZTTB8qPpQu8XO0xqqnBwAA/nPkClLyKrR6/09TruL3vArwuRysmz4UVnwa5cVcoOJD6ZYpEV6YOdwHhAAvbcvV2u4Ge87cxMdtC1n/NW4A+ruLtHJfinFAxYfSI1aO7Y9IPwc0SFsxY2MGrlY+3F7vR/Mr8epP5wAAC0b603AZZggVH0qPsOBysOG5cPRzE6G6QYoZG0/hauWDtYD2ny/DC9+dhkyuwJiBrnjtiRAtW0sxBnQmPqtXr0ZMTAysrKxgZ2enq2woesTemo/vn49CiKstKuulGL8+HQcv9HwBqlxB8MWxa1jyQw5a5ASJg93wybQwcOjaLbNEZ+Ijk8kwefJkLFq0SFdZUFjAoU2AVF2whVtzsGLXeVTWN3d53bWqBjz71Sm8d+AyFASYFuGFT6eF0VAZZgxDdDx3fvPmzVi2bBlqa2t7fa1EIoFYLEZdXR1EIuqMNCRa5Aq8f+Ayvmrb38nSgotJ4Z4Y3c8ZgzzEsOJzcaepBbkltfjl7E0cvlQBQgArPhfJY/tjyjAvuveWCfAw76hBjWtKpVJIpVL1d4lEwqI1lK6w4HLwz6f6I76/C947cBm5pbXYcqpYPUu5I+L7ueCfif3g62itR0sphopBic+aNWuwatUqts2g9ILh/n2w++8xOHqlCocvVuD4lSrcbFuKweMwCHGzxTAfBzw33BuBzrYsW0sxJHolPq+//jref//9LtPk5eUhJOTBRi9WrFiB5cuXq79LJBJ4edHYvYYOwzB4tK8zHu3rDEDpWG5ukYPHZSDg0TValI7plfi88sormD17dpdp/P39H9gYgUAAgUDwwNdTDAMuh4G1wKAa1RQDpFe/ECcnJzg5OenKFgqFYkborHoqKSnB7du3UVJSArlcjtzcXABAYGAgbGx6FiRKNRBHHc8UimGiejcfaNCc6IikpCQCoN0nNTW1x/coLS3t8B70Qz/0Y1if0tLSXmuEzuf5PAwKhQK3bt2Cra1tt3NCVM7p0tJSk5kTZGplMrXyALRMhBDU19fD3d0dHE7vJowatFeQw+HA07N3ezaJRCKT+RGoMLUymVp5APMuk1gsfqD707ntFAqFFaj4UCgUVjAZ8REIBEhOTjapeUKmViZTKw9Ay/QwGLTDmUKhmC4m0/KhUCjGBRUfCoXCClR8KBQKK1DxoVAorEDFh0KhsIJRic/69evh6+sLoVCIqKgoZGZmdpl+586dCAkJgVAoxKBBg7B//349Wdo9a9asQUREBGxtbeHs7Izx48cjPz+/y2s2b94MhmE0PkKhUE8Wd8/bb7/dzr7uYjsZ8jMCAF9f33ZlYhgGixcv7jC9oT2j48ePY+zYsXB3dwfDMNizZ4/GeUIIVq5cCTc3N1haWiI+Ph4FBQXd3re372JHGI34bN++HcuXL0dycjJycnIQGhqKhIQEVFZWdpg+PT0d06dPx7x583DmzBmMHz8e48ePx4ULF/RsecccO3YMixcvxqlTp3DkyBG0tLTgb3/7GxobG7u8TiQSoaysTP0pLu48bCkbDBgwQMO+EydOdJrW0J8RAGRlZWmU58iRIwCAyZMnd3qNIT2jxsZGhIaGYv369R2e/+CDD/Dpp5/iiy++QEZGBqytrZGQkIDm5s43BOjtu9gpvV6KyhKRkZFk8eLF6u9yuZy4u7uTNWvWdJh+ypQpJDExUeNYVFQUWbBggU7tfFAqKysJAHLs2LFO02zatImIxWL9GdVLkpOTSWhoaI/TG9szIoSQl156iQQEBBCFQtHheUN+RgDI7t271d8VCgVxdXUla9euVR+rra0lAoGA/Pjjj53ep7fvYmcYRctHJpMhOzsb8fHx6mMcDgfx8fE4efJkh9ecPHlSIz0AJCQkdJqeberq6gAADg4OXaZraGiAj48PvLy8MG7cOFy8eFEf5vWYgoICuLu7w9/fH88++yxKSko6TWtsz0gmk2Hr1q2YO3dul1EWDP0ZqSgsLER5ebnGMxCLxYiKiur0GTzIu9gZRiE+1dXVkMvlcHFx0Tju4uKC8vLyDq8pLy/vVXo2USgUWLZsGWJjYzFw4MBO0/Xt2xfffPMN9u7di61bt0KhUCAmJgY3btzQo7WdExUVhc2bN+PgwYP4/PPPUVhYiBEjRqC+vuOdTY3pGQHAnj17UFtb22UoYUN/Rvei+j/35hk8yLvYGQYdUsNcWLx4MS5cuNClfwQAoqOjER0drf4eExODfv36YcOGDXjnnXd0bWa3jBkzRv334MGDERUVBR8fH+zYsQPz5s1j0TLt8PXXX2PMmDFwd3fvNI2hPyNDwihaPo6OjuByuaioqNA4XlFRAVdX1w6vcXV17VV6tliyZAl+++03pKam9jp2kYWFBcLCwnD16lUdWfdw2NnZITg4uFP7jOUZAUBxcTF+//13PP/88726zpCfker/3Jtn8CDvYmcYhfjw+XyEh4cjJSVFfUyhUCAlJUWjlrmX6OhojfQAcOTIkU7T6xtCCJYsWYLdu3fjjz/+gJ+fX6/vIZfLcf78ebi5uenAwoenoaEB165d69Q+Q39G97Jp0yY4OzsjMTGxV9cZ8jPy8/ODq6urxjOQSCTIyMjo9Bk8yLvYKb1yT7PItm3biEAgIJs3byaXLl0i8+fPJ3Z2dqS8vJwQQsjMmTPJ66+/rk6flpZGeDwe+fDDD0leXh5JTk4mFhYW5Pz582wVQYNFixYRsVhMjh49SsrKytSfpqYmdZr7y7Rq1Spy6NAhcu3aNZKdnU2mTZtGhEIhuXjxIhtFaMcrr7xCjh49SgoLC0laWhqJj48njo6OpLKykhBifM9IhVwuJ97e3uS1115rd87Qn1F9fT05c+YMOXPmDAFAPvroI3LmzBlSXFxMCCHkvffeI3Z2dmTv3r3k3LlzZNy4ccTPz4/cvXtXfY/Ro0eTdevWqb939y72FKMRH0IIWbduHfH29iZ8Pp9ERkaSU6dOqc+NGjWKJCUlaaTfsWMHCQ4OJnw+nwwYMIDs27dPzxZ3DjoJxL1p0yZ1mvvLtGzZMnX5XVxcyJNPPklycnL0b3wnTJ06lbi5uRE+n088PDzI1KlTydWrV9Xnje0ZqTh06BABQPLz89udM/RnlJqa2uHvTGWzQqEgb731FnFxcSECgYA89thj7crp4+NDkpOTNY519S72FBrPh0KhsIJR+HwoFIrpQcWHQqGwAhUfCoXCClR8KBQKK1DxoVAorEDFh0KhsAIVHwqFwgpUfCgUCitQ8aFQKKxAxYdCobACFR8KhcIK/w9XjPDvQ4P90QAAAABJRU5ErkJggg==",
|
21
|
+
"text/plain": [
|
22
|
+
"<Figure size 300x150 with 1 Axes>"
|
23
|
+
]
|
24
|
+
},
|
25
|
+
"metadata": {},
|
26
|
+
"output_type": "display_data"
|
27
|
+
}
|
28
|
+
],
|
29
|
+
"source": [
|
30
|
+
"# #simple-plot\n",
|
31
|
+
"import matplotlib.pyplot as plt\n",
|
32
|
+
"import numpy as np\n",
|
33
|
+
"\n",
|
34
|
+
"x = np.linspace(0, 10, 100)\n",
|
35
|
+
"plt.figure(figsize=(3, 1.5))\n",
|
36
|
+
"plt.plot(x, np.sin(x))\n",
|
37
|
+
"plt.title(\"Simple Sine Wave\")"
|
38
|
+
]
|
39
|
+
},
|
40
|
+
{
|
41
|
+
"cell_type": "code",
|
42
|
+
"execution_count": null,
|
43
|
+
"metadata": {},
|
44
|
+
"outputs": [],
|
45
|
+
"source": []
|
46
|
+
}
|
47
|
+
],
|
48
|
+
"metadata": {
|
49
|
+
"kernelspec": {
|
50
|
+
"display_name": ".venv",
|
51
|
+
"language": "python",
|
52
|
+
"name": "python3"
|
53
|
+
},
|
54
|
+
"language_info": {
|
55
|
+
"codemirror_mode": {
|
56
|
+
"name": "ipython",
|
57
|
+
"version": 3
|
58
|
+
},
|
59
|
+
"file_extension": ".py",
|
60
|
+
"mimetype": "text/x-python",
|
61
|
+
"name": "python",
|
62
|
+
"nbconvert_exporter": "python",
|
63
|
+
"pygments_lexer": "ipython3",
|
64
|
+
"version": "3.13.3"
|
65
|
+
}
|
66
|
+
},
|
67
|
+
"nbformat": 4,
|
68
|
+
"nbformat_minor": 2
|
69
|
+
}
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "nbsync"
|
7
|
-
version = "0.1.
|
7
|
+
version = "0.1.1"
|
8
8
|
description = "MkDocs plugin treating Jupyter notebooks, Python scripts and Markdown files as first-class citizens for documentation with dynamic execution and real-time synchronization"
|
9
9
|
readme = "README.md"
|
10
10
|
license = { file = "LICENSE" }
|
@@ -55,6 +55,7 @@ dev = [
|
|
55
55
|
"pytest-randomly>=3.16.0",
|
56
56
|
"pytest-xdist>=3.6.1",
|
57
57
|
"ruff>=0.11.4",
|
58
|
+
"seaborn>=0.13.2",
|
58
59
|
]
|
59
60
|
|
60
61
|
[tool.pytest.ini_options]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import matplotlib.pyplot as plt
|
2
|
+
import numpy as np
|
3
|
+
|
4
|
+
|
5
|
+
def plot_sine(frequency=1):
|
6
|
+
"""Plot a sine wave with given frequency."""
|
7
|
+
x = np.linspace(0, 10, 100)
|
8
|
+
plt.figure(figsize=(2, 1.2))
|
9
|
+
plt.plot(x, np.sin(frequency * x))
|
10
|
+
plt.title(f"Sine Wave (f={frequency})")
|
11
|
+
plt.ylim(-1.2, 1.2)
|
12
|
+
|
13
|
+
|
14
|
+
def plot_histogram(bins=20):
|
15
|
+
"""Plot a histogram of random data."""
|
16
|
+
data = np.random.randn(1000)
|
17
|
+
plt.figure(figsize=(2, 1.2))
|
18
|
+
plt.hist(data, bins=bins)
|
19
|
+
plt.title(f"Histogram (bins={bins})")
|
@@ -30,22 +30,35 @@ class Cell:
|
|
30
30
|
def convert(self) -> str:
|
31
31
|
kind = self.image.attributes.pop("source", "")
|
32
32
|
tabs = self.image.attributes.pop("tabs", "")
|
33
|
+
identifier = self.image.attributes.pop("identifier", "")
|
33
34
|
|
34
35
|
if "/" not in self.mime or not self.content or kind == "source-only":
|
35
36
|
if self.image.source:
|
36
|
-
source = get_source(
|
37
|
+
source = get_source(
|
38
|
+
self,
|
39
|
+
include_attrs=True,
|
40
|
+
include_identifier=bool(identifier),
|
41
|
+
)
|
37
42
|
kind = "only"
|
38
43
|
else:
|
39
44
|
source = ""
|
40
45
|
result, self.image.url = "", ""
|
41
46
|
|
42
47
|
elif self.mime.startswith("text/") and isinstance(self.content, str):
|
43
|
-
source = get_source(
|
48
|
+
source = get_source(
|
49
|
+
self,
|
50
|
+
include_attrs=True,
|
51
|
+
include_identifier=bool(identifier),
|
52
|
+
)
|
44
53
|
result, self.image.url = self.content, ""
|
45
54
|
result = result.rstrip()
|
46
55
|
|
47
56
|
else:
|
48
|
-
source = get_source(
|
57
|
+
source = get_source(
|
58
|
+
self,
|
59
|
+
include_attrs=False,
|
60
|
+
include_identifier=bool(identifier),
|
61
|
+
)
|
49
62
|
result = get_result(self)
|
50
63
|
|
51
64
|
if markdown := get_markdown(kind, source, result, tabs):
|
@@ -54,12 +67,22 @@ class Cell:
|
|
54
67
|
return "" # no cov
|
55
68
|
|
56
69
|
|
57
|
-
def get_source(
|
70
|
+
def get_source(
|
71
|
+
cell: Cell,
|
72
|
+
*,
|
73
|
+
include_attrs: bool = False,
|
74
|
+
include_identifier: bool = False,
|
75
|
+
) -> str:
|
58
76
|
attrs = [cell.language]
|
59
77
|
if include_attrs:
|
60
78
|
attrs.extend(cell.image.iter_parts())
|
61
79
|
attr = " ".join(attrs)
|
62
|
-
|
80
|
+
|
81
|
+
source = cell.image.source
|
82
|
+
if include_identifier:
|
83
|
+
source = f"# #{cell.image.identifier}\n{source}"
|
84
|
+
|
85
|
+
return f"```{attr}\n{source}\n```"
|
63
86
|
|
64
87
|
|
65
88
|
def get_result(cell: Cell) -> str:
|
File without changes
|
@@ -54,19 +54,19 @@ def test_empty(convert):
|
|
54
54
|
def test_image(convert):
|
55
55
|
x = convert("{#fig a b=c}")
|
56
56
|
assert x.startswith("![a]")
|
57
|
-
assert x.endswith(
|
57
|
+
assert x.endswith('.png){#fig a b="c"}')
|
58
58
|
|
59
59
|
|
60
60
|
def test_func(convert):
|
61
|
-
x = convert("{#func a=b c}")
|
62
|
-
assert x ==
|
61
|
+
x = convert("{#func a=b c identifier='1'}")
|
62
|
+
assert x == '```python c a="b"\n# #func\ndef f():\n pass\n```'
|
63
63
|
|
64
64
|
|
65
65
|
@pytest.mark.parametrize("kind", ["above", "on", "1"])
|
66
66
|
def test_above(convert, kind):
|
67
|
-
x = convert(f"{{#fig source='{kind}' a b=c}}")
|
67
|
+
x = convert(f"{{#fig source='{kind}' a b='c'}}")
|
68
68
|
assert x.startswith("```python\n")
|
69
|
-
assert x.endswith(
|
69
|
+
assert x.endswith('.png){#fig a b="c"}')
|
70
70
|
|
71
71
|
|
72
72
|
def test_below(convert):
|