syd 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl

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.
@@ -0,0 +1,174 @@
1
+ class SydViewer {
2
+ constructor(config) {
3
+ this.config = {
4
+ controlsPosition: 'left',
5
+ continuous: false,
6
+ updateInterval: 200,
7
+ ...config
8
+ };
9
+
10
+ this.form = document.getElementById('controls-form');
11
+ this.plot = document.getElementById('plot');
12
+ this.updateTimer = null;
13
+ this.setupEventListeners();
14
+ this.updatePlot();
15
+ }
16
+
17
+ setupEventListeners() {
18
+ // Handle all form input changes
19
+ this.form.addEventListener('input', (event) => {
20
+ const input = event.target;
21
+ if (input.dataset.continuous === 'true' || this.config.continuous) {
22
+ this.debounceUpdate(() => this.handleInputChange(input));
23
+ }
24
+ });
25
+
26
+ // Handle form changes for non-continuous updates
27
+ this.form.addEventListener('change', (event) => {
28
+ const input = event.target;
29
+ if (input.dataset.continuous !== 'true' && !this.config.continuous) {
30
+ this.handleInputChange(input);
31
+ }
32
+ });
33
+
34
+ // Handle button clicks
35
+ this.form.querySelectorAll('button[type="button"]').forEach(button => {
36
+ button.addEventListener('click', () => this.handleButtonClick(button));
37
+ });
38
+ }
39
+
40
+ debounceUpdate(callback) {
41
+ if (this.updateTimer) {
42
+ clearTimeout(this.updateTimer);
43
+ }
44
+ this.updateTimer = setTimeout(callback, this.config.updateInterval);
45
+ }
46
+
47
+ async handleInputChange(input) {
48
+ let value = this.getInputValue(input);
49
+ const name = input.name;
50
+
51
+ try {
52
+ const response = await fetch(`/update/${name}`, {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify({ value }),
58
+ });
59
+
60
+ if (!response.ok) {
61
+ throw new Error(`HTTP error! status: ${response.status}`);
62
+ }
63
+
64
+ await this.updatePlot();
65
+ } catch (error) {
66
+ console.error('Error updating parameter:', error);
67
+ }
68
+ }
69
+
70
+ async handleButtonClick(button) {
71
+ const name = button.name;
72
+ try {
73
+ const response = await fetch(`/update/${name}`, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ },
78
+ body: JSON.stringify({ value: null }),
79
+ });
80
+
81
+ if (!response.ok) {
82
+ throw new Error(`HTTP error! status: ${response.status}`);
83
+ }
84
+
85
+ await this.updatePlot();
86
+ } catch (error) {
87
+ console.error('Error handling button click:', error);
88
+ }
89
+ }
90
+
91
+ getInputValue(input) {
92
+ switch (input.type) {
93
+ case 'checkbox':
94
+ return input.checked;
95
+ case 'number':
96
+ return parseFloat(input.value);
97
+ case 'range':
98
+ if (input.name.endsWith('_low')) {
99
+ const high = document.getElementById(input.id.replace('_low', '_high')).value;
100
+ return [parseFloat(input.value), parseFloat(high)];
101
+ } else if (input.name.endsWith('_high')) {
102
+ const low = document.getElementById(input.id.replace('_high', '_low')).value;
103
+ return [parseFloat(low), parseFloat(input.value)];
104
+ }
105
+ return parseFloat(input.value);
106
+ case 'select-multiple':
107
+ return Array.from(input.selectedOptions).map(option => {
108
+ const value = option.value;
109
+ return !isNaN(value) ? parseFloat(value) : value;
110
+ });
111
+ default:
112
+ const value = input.value;
113
+ return !isNaN(value) ? parseFloat(value) : value;
114
+ }
115
+ }
116
+
117
+ async updatePlot() {
118
+ try {
119
+ const response = await fetch('/plot');
120
+ if (!response.ok) {
121
+ throw new Error(`HTTP error! status: ${response.status}`);
122
+ }
123
+
124
+ const data = await response.json();
125
+ if (data.error) {
126
+ throw new Error(data.error);
127
+ }
128
+
129
+ this.plot.src = `data:image/png;base64,${data.image}`;
130
+ } catch (error) {
131
+ console.error('Error updating plot:', error);
132
+ }
133
+ }
134
+
135
+ async updateState() {
136
+ try {
137
+ const response = await fetch('/state');
138
+ if (!response.ok) {
139
+ throw new Error(`HTTP error! status: ${response.status}`);
140
+ }
141
+
142
+ const state = await response.json();
143
+ this.syncFormWithState(state);
144
+ } catch (error) {
145
+ console.error('Error updating state:', error);
146
+ }
147
+ }
148
+
149
+ syncFormWithState(state) {
150
+ for (const [name, value] of Object.entries(state)) {
151
+ const input = this.form.elements[name];
152
+ if (!input) continue;
153
+
154
+ if (input.type === 'checkbox') {
155
+ input.checked = value;
156
+ } else if (input.type === 'select-multiple') {
157
+ Array.from(input.options).forEach(option => {
158
+ option.selected = value.includes(option.value);
159
+ });
160
+ } else if (input.type === 'range' && Array.isArray(value)) {
161
+ const [low, high] = value;
162
+ document.getElementById(`param_${name}_low`).value = low;
163
+ document.getElementById(`param_${name}_high`).value = high;
164
+ document.querySelector(`output[for="param_${name}_low"]`).value = low;
165
+ document.querySelector(`output[for="param_${name}_high"]`).value = high;
166
+ } else {
167
+ input.value = value;
168
+ if (input.type === 'range') {
169
+ document.querySelector(`output[for="${input.id}"]`).value = value;
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
@@ -1,26 +1,29 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
- <title>{% block title %}Interactive Viewer{% endblock %}</title>
5
- {% for css in required_css %}
6
- <link rel="stylesheet" href="{{ css }}">
7
- {% endfor %}
8
- <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
9
- <style>
10
- :root {
11
- --controls-width: {{ config.controls_width_percent }}%;
12
- --plot-width: {{ 100 - config.controls_width_percent }}%;
13
- }
14
- {{ custom_styles | safe }}
15
- </style>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Syd Viewer{% endblock %}</title>
7
+
8
+ <!-- Bootstrap CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
10
+
11
+ <!-- Custom CSS -->
12
+ <link href="{{ url_for('static', filename='css/viewer.css') }}" rel="stylesheet">
13
+
14
+ {% block extra_head %}{% endblock %}
16
15
  </head>
17
16
  <body>
18
- {% block content %}{% endblock %}
17
+ <div class="container-fluid p-0">
18
+ {% block content %}{% endblock %}
19
+ </div>
20
+
21
+ <!-- Bootstrap Bundle with Popper -->
22
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
23
+
24
+ <!-- Custom JavaScript -->
25
+ <script src="{{ url_for('static', filename='js/viewer.js') }}"></script>
19
26
 
20
- {% for js in required_js %}
21
- <script src="{{ js }}"></script>
22
- {% endfor %}
23
- <script src="{{ url_for('static', filename='js/components.js') }}"></script>
24
- {% block scripts %}{% endblock %}
27
+ {% block extra_scripts %}{% endblock %}
25
28
  </body>
26
- </html>
29
+ </html>
@@ -1,97 +1,51 @@
1
- # templates/viewer.html
2
- <!DOCTYPE html>
3
- <html>
4
- <head>
5
- <title>Interactive Viewer</title>
6
- {% for css in required_css %}
7
- <link rel="stylesheet" href="{{ css }}">
8
- {% endfor %}
9
- <style>
10
- {{ custom_styles | safe }}
11
- .controls {
12
- {% if config.is_horizontal %}
13
- width: {{ config.controls_width_percent }}%;
14
- float: left;
15
- padding-right: 20px;
16
- {% endif %}
17
- }
18
- .plot-container {
19
- {% if config.is_horizontal %}
20
- width: {{ 100 - config.controls_width_percent }}%;
21
- float: left;
22
- {% endif %}
23
- }
24
- #plot {
25
- width: 100%;
26
- height: auto;
27
- }
28
- </style>
29
- </head>
30
- <body>
31
- <div class="container-fluid">
32
- <div class="row">
33
- <div class="controls">
34
- {{ components_html | safe }}
35
- </div>
36
- <div class="plot-container">
37
- <img id="plot" src="{{ initial_plot }}">
38
- </div>
1
+ {% extends "base.html" %}
2
+
3
+ {% block extra_head %}
4
+ <style>
5
+ :root {
6
+ --controls-width: {{ controls_width }}%;
7
+ --figure-width: {{ figure_width }}px;
8
+ --figure-height: {{ figure_height }}px;
9
+ }
10
+ </style>
11
+ {% endblock %}
12
+
13
+ {% block content %}
14
+ <div class="viewer-container {% if controls_position in ['left', 'right'] %}d-flex flex-row{% else %}d-flex flex-column{% endif %} h-100"
15
+ data-controls-position="{{ controls_position }}"
16
+ data-continuous="{{ continuous|default(false)|tojson }}">
17
+ {% if controls_position in ['left', 'top'] %}
18
+ <div class="controls-container {% if controls_position == 'left' %}w-controls{% else %}w-100{% endif %}">
19
+ <form id="controls-form" class="p-3">
20
+ {{ components|join('')|safe }}
21
+ </form>
39
22
  </div>
23
+ {% endif %}
24
+
25
+ <div class="plot-container {% if controls_position in ['left', 'right'] %}flex-grow-1{% else %}w-100{% endif %} d-flex justify-content-center align-items-center p-3">
26
+ <img id="plot" src="{% if initial_plot %}data:image/png;base64,{{ initial_plot }}{% endif %}" alt="Plot" class="img-fluid">
40
27
  </div>
41
-
42
- {% for js in required_js %}
43
- <script src="{{ js }}"></script>
44
- {% endfor %}
45
-
46
- <script>
47
- function updateParameter(name, value) {
48
- fetch('/update_parameter', {
49
- method: 'POST',
50
- headers: {
51
- 'Content-Type': 'application/json',
52
- },
53
- body: JSON.stringify({name, value})
54
- })
55
- .then(response => response.json())
56
- .then(data => {
57
- if (data.error) {
58
- console.error(data.error);
59
- return;
60
- }
61
- // Update plot
62
- document.getElementById('plot').src = data.plot;
63
- // Apply any parameter updates
64
- for (const [param, js] of Object.entries(data.updates)) {
65
- eval(js);
66
- }
67
- });
68
- }
69
-
70
- function buttonClick(name) {
71
- fetch('/button_click', {
72
- method: 'POST',
73
- headers: {
74
- 'Content-Type': 'application/json',
75
- },
76
- body: JSON.stringify({name})
77
- })
78
- .then(response => response.json())
79
- .then(data => {
80
- if (data.error) {
81
- console.error(data.error);
82
- return;
83
- }
84
- // Update plot
85
- document.getElementById('plot').src = data.plot;
86
- // Apply any parameter updates
87
- for (const [param, js] of Object.entries(data.updates)) {
88
- eval(js);
89
- }
90
- });
91
- }
92
-
93
- // Initialize components
94
- {{ components_init | safe }}
95
- </script>
96
- </body>
97
- </html>
28
+
29
+ {% if controls_position in ['right', 'bottom'] %}
30
+ <div class="controls-container {% if controls_position == 'right' %}w-controls{% else %}w-100{% endif %}">
31
+ <form id="controls-form" class="p-3">
32
+ {{ components|join('')|safe }}
33
+ </form>
34
+ </div>
35
+ {% endif %}
36
+ </div>
37
+ {% endblock %}
38
+
39
+ {% block extra_scripts %}
40
+ <script>
41
+ // Initialize the viewer with configuration
42
+ document.addEventListener('DOMContentLoaded', function() {
43
+ const container = document.querySelector('.viewer-container');
44
+ window.viewer = new SydViewer({
45
+ controlsPosition: container.dataset.controlsPosition,
46
+ continuous: JSON.parse(container.dataset.continuous),
47
+ updateInterval: 200
48
+ });
49
+ });
50
+ </script>
51
+ {% endblock %}
@@ -0,0 +1,300 @@
1
+ # Testing Principles for SYD Flask Deployment
2
+
3
+ ## Overview
4
+
5
+ Testing an interactive GUI application like the SYD Flask deployment presents unique challenges. While traditional test suites focus on programmatic API testing, interactive GUIs require a different approach that combines several testing methodologies. This document outlines a comprehensive testing strategy for the SYD Flask deployment.
6
+
7
+ ## Testing Pyramid for Interactive GUIs
8
+
9
+ For the SYD Flask deployment, we recommend implementing a modified testing pyramid with the following layers:
10
+
11
+ 1. **Unit Tests** - Test isolated components and functions
12
+ 2. **Integration Tests** - Test interactions between components
13
+ 3. **API Tests** - Test Flask API endpoints
14
+ 4. **Headless Browser Tests** - Test GUI interactions programmatically
15
+ 5. **Visual Regression Tests** - Ensure UI appearance remains consistent
16
+ 6. **Manual Testing Checklists** - Structured human verification
17
+
18
+ ## 1. Unit Tests
19
+
20
+ ### What to Test
21
+ - Individual Flask routes
22
+ - Component HTML generation functions
23
+ - Parameter update handlers
24
+ - State synchronization logic
25
+ - Plot generation utilities
26
+
27
+ ### Implementation Strategy
28
+ ```python
29
+ import unittest
30
+ from unittest import mock
31
+ from syd.flask_deployment.deployer import FlaskDeployer
32
+ from syd.flask_deployment.components import create_component
33
+
34
+ class TestFlaskDeployerComponents(unittest.TestCase):
35
+ def setUp(self):
36
+ self.mock_viewer = mock.MagicMock()
37
+ self.deployer = FlaskDeployer(self.mock_viewer)
38
+
39
+ def test_create_float_component(self):
40
+ mock_param = mock.MagicMock()
41
+ mock_param.name = "test_param"
42
+ mock_param.value = 5.0
43
+ component = create_component(mock_param)
44
+ self.assertIn("test_param", component.html)
45
+ self.assertIn("5.0", component.html)
46
+ ```
47
+
48
+ ## 2. Integration Tests
49
+
50
+ ### What to Test
51
+ - Parameter creation to HTML rendering pipeline
52
+ - Parameter update → state sync → component update flow
53
+ - Plot generation and serving workflow
54
+
55
+ ### Implementation Strategy
56
+ ```python
57
+ def test_parameter_update_state_sync():
58
+ # Create real viewer with test parameters
59
+ viewer = Viewer()
60
+ viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
61
+
62
+ # Create deployer with this viewer
63
+ deployer = FlaskDeployer(viewer)
64
+
65
+ # Update parameter and check if components reflect the change
66
+ deployer._handle_parameter_update('test_param', 5.0)
67
+
68
+ # Verify component has updated
69
+ component = deployer.parameter_components['test_param']
70
+ self.assertEqual(component.value, 5.0)
71
+ ```
72
+
73
+ ## 3. API Tests
74
+
75
+ ### What to Test
76
+ - All Flask endpoints (`/`, `/update/<name>`, `/state`, `/plot`)
77
+ - Response formats and status codes
78
+ - Error handling
79
+ - Race conditions with concurrent requests
80
+
81
+ ### Implementation Strategy
82
+ ```python
83
+ def test_update_parameter_endpoint():
84
+ viewer = Viewer()
85
+ viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
86
+ deployer = FlaskDeployer(viewer)
87
+ app = deployer.app
88
+
89
+ with app.test_client() as client:
90
+ # Test successful update
91
+ response = client.post('/update/test_param',
92
+ json={'value': 5.0})
93
+ self.assertEqual(response.status_code, 200)
94
+ self.assertEqual(viewer.state['test_param'], 5.0)
95
+
96
+ # Test parameter not found
97
+ response = client.post('/update/nonexistent',
98
+ json={'value': 5.0})
99
+ self.assertEqual(response.status_code, 404)
100
+ ```
101
+
102
+ ## 4. Headless Browser Tests
103
+
104
+ ### What to Test
105
+ - User interactions with the web interface
106
+ - Parameter widget interactions
107
+ - UI updates in response to parameter changes
108
+ - Plot refreshes
109
+
110
+ ### Implementation Strategy
111
+
112
+ Use Selenium or Playwright to automate browser interactions:
113
+
114
+ ```python
115
+ from selenium import webdriver
116
+ from selenium.webdriver.common.by import By
117
+
118
+ def test_slider_interaction():
119
+ # Start the Flask app in a separate thread
120
+ viewer = create_test_viewer()
121
+ deployer = FlaskDeployer(viewer)
122
+ thread = threading.Thread(target=deployer.app.run)
123
+ thread.daemon = True
124
+ thread.start()
125
+
126
+ # Use Selenium to interact with the web interface
127
+ driver = webdriver.Chrome()
128
+ driver.get('http://localhost:5000')
129
+
130
+ # Find a slider and interact with it
131
+ slider = driver.find_element(By.ID, 'param_amplitude')
132
+ slider.send_keys(webdriver.Keys.ARROW_RIGHT * 5) # Increase value
133
+
134
+ # Verify the plot updates
135
+ # This requires some way to detect that the plot has changed
136
+ # Could check the src attribute of the image element changes
137
+ time.sleep(1) # Allow time for update
138
+ plot_src = driver.find_element(By.ID, 'plot').get_attribute('src')
139
+ self.assertTrue(len(plot_src) > 0)
140
+
141
+ driver.quit()
142
+ ```
143
+
144
+ ## 5. Visual Regression Tests
145
+
146
+ ### What to Test
147
+ - UI appearance remains consistent across changes
148
+ - Components render correctly with different values
149
+ - Layout adapts properly to different screen sizes
150
+
151
+ ### Implementation Strategy
152
+
153
+ Use tools like Percy or BackstopJS to capture and compare screenshots:
154
+
155
+ ```python
156
+ def test_visual_appearance():
157
+ # Set up test app with standard parameters
158
+ app = create_standard_test_app()
159
+
160
+ # Use a tool like Percy
161
+ percy_snapshot(driver, 'Main view')
162
+
163
+ # Change to different layout
164
+ driver.get('http://localhost:5000?controls_position=right')
165
+ percy_snapshot(driver, 'Right controls layout')
166
+
167
+ # Test responsive views
168
+ driver.set_window_size(400, 800) # Mobile size
169
+ percy_snapshot(driver, 'Mobile view')
170
+ ```
171
+
172
+ ## 6. Manual Testing Checklists
173
+
174
+ For some aspects of interactive GUI applications, manual testing remains necessary. Create structured checklists for testers:
175
+
176
+ ```markdown
177
+ # Manual Testing Checklist
178
+
179
+ ## Parameter Interactions
180
+ - [ ] Sliders respond smoothly to mouse drag
181
+ - [ ] Number inputs accept keyboard input
182
+ - [ ] Range sliders correctly display and update both values
183
+ - [ ] Toggles switch state when clicked
184
+ - [ ] Dropdowns show all options and select correctly
185
+
186
+ ## Plot Updates
187
+ - [ ] Plot updates immediately after parameter change when continuous=True
188
+ - [ ] Plot updates only after interaction ends when continuous=False
189
+ - [ ] Plot maintains aspect ratio when window is resized
190
+ - [ ] No visual glitches during plot transitions
191
+
192
+ ## Layouts
193
+ - [ ] Test all layouts: left, right, top, bottom
194
+ - [ ] Verify responsive behavior on different screen sizes
195
+ ```
196
+
197
+ ## Automated Testing Framework
198
+
199
+ To bring this all together, we recommend implementing a pytest-based testing framework:
200
+
201
+ ```python
202
+ # conftest.py
203
+ import pytest
204
+ from syd import make_viewer
205
+ import numpy as np
206
+ import matplotlib.pyplot as plt
207
+
208
+ @pytest.fixture
209
+ def standard_test_viewer():
210
+ """Create a standard viewer for testing."""
211
+ viewer = make_viewer()
212
+
213
+ def plot(state):
214
+ fig, ax = plt.subplots()
215
+ x = np.linspace(0, 10, 1000)
216
+ y = state['amplitude'] * np.sin(state['frequency'] * x)
217
+ ax.plot(x, y)
218
+ return fig
219
+
220
+ viewer.set_plot(plot)
221
+ viewer.add_float('amplitude', value=1.0, min_value=0, max_value=2)
222
+ viewer.add_float('frequency', value=1.0, min_value=0.1, max_value=5)
223
+
224
+ return viewer
225
+
226
+ @pytest.fixture
227
+ def flask_app(standard_test_viewer):
228
+ """Create a Flask app for testing."""
229
+ from syd.flask_deployment.deployer import FlaskDeployer
230
+ deployer = FlaskDeployer(standard_test_viewer)
231
+ return deployer.app
232
+ ```
233
+
234
+ ## Continuous Integration Setup
235
+
236
+ Integrate these tests into a CI pipeline:
237
+
238
+ ```yaml
239
+ # .github/workflows/flask-tests.yml
240
+ name: Flask Deployment Tests
241
+
242
+ on:
243
+ push:
244
+ paths:
245
+ - 'syd/flask_deployment/**'
246
+ - 'tests/flask/**'
247
+
248
+ jobs:
249
+ test:
250
+ runs-on: ubuntu-latest
251
+ steps:
252
+ - uses: actions/checkout@v2
253
+ - name: Set up Python
254
+ uses: actions/setup-python@v2
255
+ with:
256
+ python-version: '3.9'
257
+ - name: Install dependencies
258
+ run: |
259
+ python -m pip install --upgrade pip
260
+ pip install -e ".[test]"
261
+ - name: Run unit and integration tests
262
+ run: pytest tests/flask/test_unit_integration.py
263
+ - name: Run API tests
264
+ run: pytest tests/flask/test_api.py
265
+ - name: Set up Chrome WebDriver
266
+ uses: browser-actions/setup-chrome@latest
267
+ - name: Run headless browser tests
268
+ run: pytest tests/flask/test_browser.py
269
+ - name: Upload visual test artifacts
270
+ uses: actions/upload-artifact@v2
271
+ with:
272
+ name: visual-test-results
273
+ path: tests/flask/visual/results
274
+ ```
275
+
276
+ ## Practical Implementation Plan
277
+
278
+ For the SYD Flask deployment, we recommend implementing tests in this order:
279
+
280
+ 1. **Start with Unit Tests** - Focus on individual components and functions
281
+ 2. **Add API Tests** - Ensure all endpoints work correctly
282
+ 3. **Implement Integration Tests** - Test component interactions
283
+ 4. **Add Visual Testing** - Simple screenshot comparisons
284
+ 5. **Create Browser Tests** - For critical UI workflows
285
+
286
+ ## Testing Challenges and Solutions
287
+
288
+ | Challenge | Solution |
289
+ |-----------|----------|
290
+ | Testing real-time updates | Use JavaScript triggers and MutationObserver in browser tests |
291
+ | Asynchronous plot generation | Implement wait utilities with timeouts |
292
+ | Visual inconsistencies across platforms | Use Percy or similar to handle cross-platform rendering |
293
+ | Slow browser tests | Run only critical UI tests in CI, more comprehensive ones nightly |
294
+ | Matplotlib backend differences | Mock or standardize backends during testing |
295
+
296
+ ## Conclusion
297
+
298
+ Testing an interactive GUI like the SYD Flask deployment requires a multi-layered approach. By combining traditional unit and integration tests with specialized GUI testing tools, we can achieve good test coverage while still accommodating the unique challenges of interactive web applications. The most effective strategy is to automate as much as possible while maintaining a structured approach to manual testing for aspects that are difficult to verify programmatically.
299
+
300
+ For the initial implementation of the testing framework, focus on establishing solid unit and API tests, as these will provide the foundation for more complex tests and help catch the most common issues early in the development process.
@@ -1 +1 @@
1
- from .deployer import NotebookDeployment
1
+ from .deployer import NotebookDeployer