figpack 0.1.4__tar.gz → 0.1.6__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.
Potentially problematic release.
This version of figpack might be problematic. Click here for more details.
- {figpack-0.1.4/figpack.egg-info → figpack-0.1.6}/PKG-INFO +49 -8
- figpack-0.1.6/README.md +123 -0
- figpack-0.1.6/figpack/__init__.py +5 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/cli.py +8 -7
- {figpack-0.1.4 → figpack-0.1.6}/figpack/core/_bundle_utils.py +2 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/core/_show_view.py +8 -12
- figpack-0.1.6/figpack/core/_upload_bundle.py +343 -0
- figpack-0.1.6/figpack/core/config.py +1 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/core/figpack_view.py +1 -0
- figpack-0.1.4/figpack/figpack-gui-dist/assets/index-Dw14QqeQ.js → figpack-0.1.6/figpack/figpack-gui-dist/assets/index-DaeClgi6.js +91 -91
- {figpack-0.1.4 → figpack-0.1.6}/figpack/figpack-gui-dist/index.html +1 -1
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/AutocorrelogramItem.py +1 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/Autocorrelograms.py +39 -9
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/CrossCorrelogramItem.py +1 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/CrossCorrelograms.py +45 -7
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/UnitsTable.py +5 -3
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/UnitsTableRow.py +1 -1
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/Box.py +9 -1
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/Image.py +12 -2
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/LayoutItem.py +1 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/Markdown.py +1 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/MatplotlibFigure.py +4 -2
- figpack-0.1.6/figpack/views/MultiChannelTimeseries.py +230 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/PlotlyFigure.py +5 -3
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/Splitter.py +3 -1
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/TabLayout.py +3 -1
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/TabLayoutItem.py +1 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/TimeseriesGraph.py +3 -2
- {figpack-0.1.4 → figpack-0.1.6}/figpack/views/__init__.py +7 -6
- {figpack-0.1.4 → figpack-0.1.6/figpack.egg-info}/PKG-INFO +49 -8
- {figpack-0.1.4 → figpack-0.1.6}/figpack.egg-info/SOURCES.txt +20 -2
- figpack-0.1.6/figpack.egg-info/requires.txt +18 -0
- figpack-0.1.6/pyproject.toml +130 -0
- figpack-0.1.6/tests/test_box.py +62 -0
- figpack-0.1.6/tests/test_cli.py +275 -0
- figpack-0.1.6/tests/test_core.py +62 -0
- figpack-0.1.6/tests/test_figpack_view.py +106 -0
- figpack-0.1.6/tests/test_image.py +60 -0
- figpack-0.1.6/tests/test_markdown.py +65 -0
- figpack-0.1.6/tests/test_matplotlib_figure.py +111 -0
- figpack-0.1.6/tests/test_multichannel_timeseries.py +221 -0
- figpack-0.1.6/tests/test_plotly_figure.py +124 -0
- figpack-0.1.6/tests/test_show_view.py +521 -0
- figpack-0.1.6/tests/test_spike_sorting_correlograms.py +133 -0
- figpack-0.1.6/tests/test_splitter.py +108 -0
- figpack-0.1.6/tests/test_tablayout.py +150 -0
- figpack-0.1.6/tests/test_timeseries_graph.py +275 -0
- figpack-0.1.6/tests/test_units_table.py +172 -0
- figpack-0.1.6/tests/test_upload_bundle.py +679 -0
- figpack-0.1.4/README.md +0 -95
- figpack-0.1.4/figpack/__init__.py +0 -9
- figpack-0.1.4/figpack/core/_upload_bundle.py +0 -451
- figpack-0.1.4/figpack.egg-info/requires.txt +0 -3
- figpack-0.1.4/pyproject.toml +0 -48
- {figpack-0.1.4 → figpack-0.1.6}/LICENSE +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/MANIFEST.in +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/core/__init__.py +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/figpack-gui-dist/assets/index-BDa2iJW9.css +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/figpack-gui-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/__init__.py +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/UnitSimilarityScore.py +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/UnitsTableColumn.py +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/__init__.py +2 -2
- {figpack-0.1.4 → figpack-0.1.6}/figpack.egg-info/dependency_links.txt +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack.egg-info/entry_points.txt +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/figpack.egg-info/top_level.txt +0 -0
- {figpack-0.1.4 → figpack-0.1.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: figpack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A Python package for creating shareable, interactive visualizations in the browser
|
|
5
5
|
Author-email: Jeremy Magland <jmagland@flatironinstitute.org>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -27,6 +27,19 @@ License-File: LICENSE
|
|
|
27
27
|
Requires-Dist: numpy
|
|
28
28
|
Requires-Dist: zarr
|
|
29
29
|
Requires-Dist: requests
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-mock>=3.10; extra == "test"
|
|
34
|
+
Requires-Dist: spikeinterface; extra == "test"
|
|
35
|
+
Requires-Dist: matplotlib; extra == "test"
|
|
36
|
+
Requires-Dist: plotly; extra == "test"
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-mock>=3.10; extra == "dev"
|
|
41
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
30
43
|
Dynamic: license-file
|
|
31
44
|
|
|
32
45
|
# figpack
|
|
@@ -68,32 +81,60 @@ y = np.sin(2 * np.pi * t)
|
|
|
68
81
|
graph.add_line_series(name="sine wave", t=t, y=y, color="blue")
|
|
69
82
|
|
|
70
83
|
# Display the visualization
|
|
71
|
-
graph.show(open_in_browser=True)
|
|
84
|
+
graph.show(open_in_browser=True, title="Quick Start Example")
|
|
72
85
|
```
|
|
73
86
|
|
|
87
|
+
## Available Views
|
|
88
|
+
|
|
89
|
+
figpack provides a comprehensive set of view components for creating interactive visualizations:
|
|
90
|
+
|
|
91
|
+
### Core Views
|
|
92
|
+
|
|
93
|
+
- **[TimeseriesGraph](docs/timeseries-graph.md)** - Interactive line plots, markers, and intervals ([example](examples/example_timeseries_graph.py))
|
|
94
|
+
- **[MultiChannelTimeseries](docs/multichannel-timeseries.md)** - Multi-channel timeseries visualization ([example](examples/example_multichannel_timeseries.py))
|
|
95
|
+
- **[Image](docs/image.md)** - Display images with optional annotations ([example](examples/example_image.py))
|
|
96
|
+
- **[Markdown](docs/markdown.md)** - Render markdown content ([example](examples/example_markdown.py))
|
|
97
|
+
|
|
98
|
+
### Layout Views
|
|
99
|
+
|
|
100
|
+
- **[Box](docs/box.md)** - Flexible container with horizontal/vertical layouts ([example](examples/example_box.py))
|
|
101
|
+
- **[Splitter](docs/splitter.md)** - Resizable split panes ([example](examples/example_splitter.py))
|
|
102
|
+
- **[TabLayout](docs/tab-layout.md)** - Tabbed interface for multiple views ([example](examples/example_tablayout.py))
|
|
103
|
+
|
|
104
|
+
### External Figure Support
|
|
105
|
+
|
|
106
|
+
- **[MatplotlibFigure](docs/matplotlib-figure.md)** - Embed matplotlib plots ([example](examples/example_matplotlib.py))
|
|
107
|
+
- **[PlotlyFigure](docs/plotly-figure.md)** - Embed plotly visualizations ([example](examples/example_plotly.py))
|
|
108
|
+
|
|
109
|
+
### Spike Sorting Views
|
|
110
|
+
|
|
111
|
+
- **[Autocorrelograms](docs/autocorrelograms.md)** - Auto-correlation analysis ([example](examples/example_autocorrelograms.py))
|
|
112
|
+
- **[CrossCorrelograms](docs/cross-correlograms.md)** - Cross-correlation analysis ([example](examples/example_cross_correlograms.py))
|
|
113
|
+
- **[UnitsTable](docs/units-table.md)** - Sortable table for spike sorting units ([example](examples/example_units_table.py))
|
|
114
|
+
|
|
74
115
|
## Examples
|
|
75
116
|
|
|
76
|
-
See the `examples/` directory.
|
|
117
|
+
See the `examples/` directory for working examples of each view type.
|
|
77
118
|
|
|
78
119
|
## Usage Modes
|
|
79
120
|
|
|
80
|
-
### Local
|
|
121
|
+
### Local-only Mode
|
|
81
122
|
|
|
82
123
|
```python
|
|
83
|
-
view.show(open_in_browser=True)
|
|
124
|
+
view.show(open_in_browser=True, title="Local Visualization")
|
|
84
125
|
```
|
|
85
126
|
|
|
86
127
|
### Sharing Online
|
|
87
128
|
|
|
88
|
-
Set the `
|
|
129
|
+
Set the `FIGPACK_API_KEY` environment variable and use:
|
|
89
130
|
|
|
90
131
|
```python
|
|
91
|
-
view.show(upload=True, open_in_browser=True)
|
|
132
|
+
view.show(upload=True, open_in_browser=True, title="Shared Visualization")
|
|
92
133
|
```
|
|
93
134
|
|
|
94
135
|
### Development Mode
|
|
95
136
|
|
|
96
|
-
Set `_dev=True` in the call to show() to enable development mode, which allows for live updates and
|
|
137
|
+
Set `_dev=True` in the call to show() to enable development mode, which allows for live updates and development of figpack-gui.
|
|
97
138
|
|
|
98
139
|
## Command Line Interface
|
|
99
140
|
|
figpack-0.1.6/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# figpack
|
|
2
|
+
|
|
3
|
+
A Python package for creating shareable, interactive visualizations in the browser.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
figpack enables you to create interactive data visualizations that can be displayed in a web browser and optionally shared online. The package focuses on timeseries data visualization with support for complex, nested layouts.
|
|
8
|
+
|
|
9
|
+
### Key Features
|
|
10
|
+
|
|
11
|
+
- **Interactive timeseries graphs** with line series, markers, and interval plots
|
|
12
|
+
- **Flexible layout system** with boxes, splitters, and tab layouts
|
|
13
|
+
- **Web-based rendering** that works in any modern browser
|
|
14
|
+
- **Shareable visualizations** that can be uploaded and shared via URLs
|
|
15
|
+
- **Zarr-based data storage** for efficient handling of large datasets
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Install figpack using pip:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install figpack
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import numpy as np
|
|
29
|
+
import figpack.views as vv
|
|
30
|
+
|
|
31
|
+
# Create a timeseries graph
|
|
32
|
+
graph = vv.TimeseriesGraph(y_label="Signal")
|
|
33
|
+
|
|
34
|
+
# Add some data
|
|
35
|
+
t = np.linspace(0, 10, 1000)
|
|
36
|
+
y = np.sin(2 * np.pi * t)
|
|
37
|
+
graph.add_line_series(name="sine wave", t=t, y=y, color="blue")
|
|
38
|
+
|
|
39
|
+
# Display the visualization
|
|
40
|
+
graph.show(open_in_browser=True, title="Quick Start Example")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Available Views
|
|
44
|
+
|
|
45
|
+
figpack provides a comprehensive set of view components for creating interactive visualizations:
|
|
46
|
+
|
|
47
|
+
### Core Views
|
|
48
|
+
|
|
49
|
+
- **[TimeseriesGraph](docs/timeseries-graph.md)** - Interactive line plots, markers, and intervals ([example](examples/example_timeseries_graph.py))
|
|
50
|
+
- **[MultiChannelTimeseries](docs/multichannel-timeseries.md)** - Multi-channel timeseries visualization ([example](examples/example_multichannel_timeseries.py))
|
|
51
|
+
- **[Image](docs/image.md)** - Display images with optional annotations ([example](examples/example_image.py))
|
|
52
|
+
- **[Markdown](docs/markdown.md)** - Render markdown content ([example](examples/example_markdown.py))
|
|
53
|
+
|
|
54
|
+
### Layout Views
|
|
55
|
+
|
|
56
|
+
- **[Box](docs/box.md)** - Flexible container with horizontal/vertical layouts ([example](examples/example_box.py))
|
|
57
|
+
- **[Splitter](docs/splitter.md)** - Resizable split panes ([example](examples/example_splitter.py))
|
|
58
|
+
- **[TabLayout](docs/tab-layout.md)** - Tabbed interface for multiple views ([example](examples/example_tablayout.py))
|
|
59
|
+
|
|
60
|
+
### External Figure Support
|
|
61
|
+
|
|
62
|
+
- **[MatplotlibFigure](docs/matplotlib-figure.md)** - Embed matplotlib plots ([example](examples/example_matplotlib.py))
|
|
63
|
+
- **[PlotlyFigure](docs/plotly-figure.md)** - Embed plotly visualizations ([example](examples/example_plotly.py))
|
|
64
|
+
|
|
65
|
+
### Spike Sorting Views
|
|
66
|
+
|
|
67
|
+
- **[Autocorrelograms](docs/autocorrelograms.md)** - Auto-correlation analysis ([example](examples/example_autocorrelograms.py))
|
|
68
|
+
- **[CrossCorrelograms](docs/cross-correlograms.md)** - Cross-correlation analysis ([example](examples/example_cross_correlograms.py))
|
|
69
|
+
- **[UnitsTable](docs/units-table.md)** - Sortable table for spike sorting units ([example](examples/example_units_table.py))
|
|
70
|
+
|
|
71
|
+
## Examples
|
|
72
|
+
|
|
73
|
+
See the `examples/` directory for working examples of each view type.
|
|
74
|
+
|
|
75
|
+
## Usage Modes
|
|
76
|
+
|
|
77
|
+
### Local-only Mode
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
view.show(open_in_browser=True, title="Local Visualization")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Sharing Online
|
|
84
|
+
|
|
85
|
+
Set the `FIGPACK_API_KEY` environment variable and use:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
view.show(upload=True, open_in_browser=True, title="Shared Visualization")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Development Mode
|
|
92
|
+
|
|
93
|
+
Set `_dev=True` in the call to show() to enable development mode, which allows for live updates and development of figpack-gui.
|
|
94
|
+
|
|
95
|
+
## Command Line Interface
|
|
96
|
+
|
|
97
|
+
figpack includes a command-line interface for working with figures:
|
|
98
|
+
|
|
99
|
+
### Download a Figure
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
figpack download <figure-url> <dest.tar.gz>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Download a figure from any figpack URL and save it as a local archive.
|
|
106
|
+
|
|
107
|
+
### View a Figure Archive
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
figpack view <figure.tar.gz>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Extract and view a figure archive in your browser. The server will run locally until you press Enter.
|
|
114
|
+
|
|
115
|
+
Use `--port <number>` to specify a custom port.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
Apache-2.0
|
|
120
|
+
|
|
121
|
+
## Contributing
|
|
122
|
+
|
|
123
|
+
Visit the [GitHub repository](https://github.com/magland/figpack) for issues, contributions, and the latest updates.
|
|
@@ -3,19 +3,20 @@ Command-line interface for figpack
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
-
import
|
|
6
|
+
import json
|
|
7
7
|
import pathlib
|
|
8
|
-
import
|
|
8
|
+
import socket
|
|
9
|
+
import sys
|
|
9
10
|
import tarfile
|
|
10
|
-
import
|
|
11
|
-
import requests
|
|
11
|
+
import tempfile
|
|
12
12
|
import threading
|
|
13
13
|
import webbrowser
|
|
14
|
-
import socket
|
|
15
14
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
16
|
-
from urllib.parse import urlparse, urljoin
|
|
17
|
-
from typing import Dict, List, Tuple, Optional, Union
|
|
18
15
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
16
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
17
|
+
from urllib.parse import urljoin, urlparse
|
|
18
|
+
|
|
19
|
+
import requests
|
|
19
20
|
|
|
20
21
|
from . import __version__
|
|
21
22
|
from .core._show_view import serve_files
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
3
|
-
from typing import Union
|
|
4
|
-
import tempfile
|
|
5
|
-
|
|
6
|
-
import webbrowser
|
|
7
|
-
|
|
8
2
|
import pathlib
|
|
9
|
-
|
|
3
|
+
import tempfile
|
|
10
4
|
import threading
|
|
5
|
+
import webbrowser
|
|
11
6
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
7
|
+
from typing import Union
|
|
12
8
|
|
|
13
|
-
from .figpack_view import FigpackView
|
|
14
9
|
from ._bundle_utils import prepare_figure_bundle
|
|
15
10
|
from ._upload_bundle import _upload_bundle
|
|
11
|
+
from .figpack_view import FigpackView
|
|
16
12
|
|
|
17
13
|
thisdir = pathlib.Path(__file__).parent.resolve()
|
|
18
14
|
|
|
@@ -32,15 +28,15 @@ def _show_view(
|
|
|
32
28
|
|
|
33
29
|
if upload:
|
|
34
30
|
# Check for required environment variable
|
|
35
|
-
|
|
36
|
-
if not
|
|
31
|
+
api_key = os.environ.get("FIGPACK_API_KEY")
|
|
32
|
+
if not api_key:
|
|
37
33
|
raise EnvironmentError(
|
|
38
|
-
"
|
|
34
|
+
"FIGPACK_API_KEY environment variable must be set to upload views."
|
|
39
35
|
)
|
|
40
36
|
|
|
41
37
|
# Upload the bundle
|
|
42
38
|
print("Starting upload...")
|
|
43
|
-
figure_url = _upload_bundle(tmpdir,
|
|
39
|
+
figure_url = _upload_bundle(tmpdir, api_key)
|
|
44
40
|
|
|
45
41
|
if open_in_browser:
|
|
46
42
|
webbrowser.open(figure_url)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import pathlib
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .. import __version__
|
|
13
|
+
|
|
14
|
+
from .config import FIGPACK_API_BASE_URL
|
|
15
|
+
|
|
16
|
+
thisdir = pathlib.Path(__file__).parent.resolve()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _upload_single_file(
|
|
20
|
+
figure_url: str, relative_path: str, file_path: pathlib.Path, api_key: str
|
|
21
|
+
) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Worker function to upload a single file using signed URL
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
str: The relative path of the uploaded file
|
|
27
|
+
"""
|
|
28
|
+
file_size = file_path.stat().st_size
|
|
29
|
+
|
|
30
|
+
# Get signed URL
|
|
31
|
+
payload = {
|
|
32
|
+
"figureUrl": figure_url,
|
|
33
|
+
"relativePath": relative_path,
|
|
34
|
+
"apiKey": api_key,
|
|
35
|
+
"size": file_size,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/upload", json=payload)
|
|
39
|
+
|
|
40
|
+
if not response.ok:
|
|
41
|
+
try:
|
|
42
|
+
error_data = response.json()
|
|
43
|
+
error_msg = error_data.get("message", "Unknown error")
|
|
44
|
+
except:
|
|
45
|
+
error_msg = f"HTTP {response.status_code}"
|
|
46
|
+
raise Exception(f"Failed to get signed URL for {relative_path}: {error_msg}")
|
|
47
|
+
|
|
48
|
+
response_data = response.json()
|
|
49
|
+
if not response_data.get("success"):
|
|
50
|
+
raise Exception(
|
|
51
|
+
f"Failed to get signed URL for {relative_path}: {response_data.get('message', 'Unknown error')}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
signed_url = response_data.get("signedUrl")
|
|
55
|
+
if not signed_url:
|
|
56
|
+
raise Exception(f"No signed URL returned for {relative_path}")
|
|
57
|
+
|
|
58
|
+
# Upload file to signed URL
|
|
59
|
+
content_type = _determine_content_type(relative_path)
|
|
60
|
+
with open(file_path, "rb") as f:
|
|
61
|
+
upload_response = requests.put(
|
|
62
|
+
signed_url, data=f, headers={"Content-Type": content_type}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not upload_response.ok:
|
|
66
|
+
raise Exception(
|
|
67
|
+
f"Failed to upload {relative_path} to signed URL: HTTP {upload_response.status_code}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return relative_path
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
MAX_WORKERS_FOR_UPLOAD = 16
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _compute_deterministic_figure_hash(tmpdir_path: pathlib.Path) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Compute a deterministic figure ID based on SHA1 hashes of all files
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
str: 40-character SHA1 hash representing the content of all files
|
|
82
|
+
"""
|
|
83
|
+
file_hashes = []
|
|
84
|
+
|
|
85
|
+
# Collect all files and their hashes
|
|
86
|
+
for file_path in sorted(tmpdir_path.rglob("*")):
|
|
87
|
+
if file_path.is_file():
|
|
88
|
+
relative_path = file_path.relative_to(tmpdir_path)
|
|
89
|
+
|
|
90
|
+
# Compute SHA1 hash of file content
|
|
91
|
+
sha1_hash = hashlib.sha1()
|
|
92
|
+
with open(file_path, "rb") as f:
|
|
93
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
94
|
+
sha1_hash.update(chunk)
|
|
95
|
+
|
|
96
|
+
# Include both the relative path and content hash to ensure uniqueness
|
|
97
|
+
file_info = f"{relative_path}:{sha1_hash.hexdigest()}"
|
|
98
|
+
file_hashes.append(file_info)
|
|
99
|
+
|
|
100
|
+
# Create final hash from all file hashes
|
|
101
|
+
combined_hash = hashlib.sha1()
|
|
102
|
+
for file_hash in file_hashes:
|
|
103
|
+
combined_hash.update(file_hash.encode("utf-8"))
|
|
104
|
+
|
|
105
|
+
return combined_hash.hexdigest()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _create_or_get_figure(
|
|
109
|
+
figure_hash: str, api_key: str, total_files: int = None, total_size: int = None
|
|
110
|
+
) -> dict:
|
|
111
|
+
"""
|
|
112
|
+
Create a new figure or get existing figure information
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
dict: Figure information from the API
|
|
116
|
+
"""
|
|
117
|
+
payload = {
|
|
118
|
+
"figureHash": figure_hash,
|
|
119
|
+
"apiKey": api_key,
|
|
120
|
+
"figpackVersion": __version__,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if total_files is not None:
|
|
124
|
+
payload["totalFiles"] = total_files
|
|
125
|
+
if total_size is not None:
|
|
126
|
+
payload["totalSize"] = total_size
|
|
127
|
+
|
|
128
|
+
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/figures/create", json=payload)
|
|
129
|
+
|
|
130
|
+
if not response.ok:
|
|
131
|
+
try:
|
|
132
|
+
error_data = response.json()
|
|
133
|
+
error_msg = error_data.get("message", "Unknown error")
|
|
134
|
+
except:
|
|
135
|
+
error_msg = f"HTTP {response.status_code}"
|
|
136
|
+
raise Exception(f"Failed to create figure {figure_hash}: {error_msg}")
|
|
137
|
+
|
|
138
|
+
response_data = response.json()
|
|
139
|
+
if not response_data.get("success"):
|
|
140
|
+
raise Exception(
|
|
141
|
+
f"Failed to create figure {figure_hash}: {response_data.get('message', 'Unknown error')}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return response_data
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _finalize_figure(figure_url: str, api_key: str) -> dict:
|
|
148
|
+
"""
|
|
149
|
+
Finalize a figure upload
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
dict: Figure information from the API
|
|
153
|
+
"""
|
|
154
|
+
payload = {
|
|
155
|
+
"figureUrl": figure_url,
|
|
156
|
+
"apiKey": api_key,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
response = requests.post(
|
|
160
|
+
f"{FIGPACK_API_BASE_URL}/api/figures/finalize", json=payload
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if not response.ok:
|
|
164
|
+
try:
|
|
165
|
+
error_data = response.json()
|
|
166
|
+
error_msg = error_data.get("message", "Unknown error")
|
|
167
|
+
except:
|
|
168
|
+
error_msg = f"HTTP {response.status_code}"
|
|
169
|
+
raise Exception(f"Failed to finalize figure {figure_url}: {error_msg}")
|
|
170
|
+
|
|
171
|
+
response_data = response.json()
|
|
172
|
+
if not response_data.get("success"):
|
|
173
|
+
raise Exception(
|
|
174
|
+
f"Failed to finalize figure {figure_url}: {response_data.get('message', 'Unknown error')}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return response_data
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _upload_bundle(tmpdir: str, api_key: str) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Upload the prepared bundle to the cloud using the new database-driven approach
|
|
183
|
+
"""
|
|
184
|
+
tmpdir_path = pathlib.Path(tmpdir)
|
|
185
|
+
|
|
186
|
+
# Compute deterministic figure ID based on file contents
|
|
187
|
+
print("Computing deterministic figure ID...")
|
|
188
|
+
figure_hash = _compute_deterministic_figure_hash(tmpdir_path)
|
|
189
|
+
print(f"Figure hash: {figure_hash}")
|
|
190
|
+
|
|
191
|
+
# Collect all files to upload
|
|
192
|
+
all_files = []
|
|
193
|
+
for file_path in tmpdir_path.rglob("*"):
|
|
194
|
+
if file_path.is_file():
|
|
195
|
+
relative_path = file_path.relative_to(tmpdir_path)
|
|
196
|
+
all_files.append((str(relative_path), file_path))
|
|
197
|
+
|
|
198
|
+
# Calculate total files and size for metadata
|
|
199
|
+
total_files = len(all_files)
|
|
200
|
+
total_size = sum(file_path.stat().st_size for _, file_path in all_files)
|
|
201
|
+
print(
|
|
202
|
+
f"Found {total_files} files to upload, total size: {total_size / (1024 * 1024):.2f} MB"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Find available figure ID and create/get figure in database with metadata
|
|
206
|
+
result = _create_or_get_figure(figure_hash, api_key, total_files, total_size)
|
|
207
|
+
figure_info = result.get("figure", {})
|
|
208
|
+
figure_url = figure_info.get("figureUrl")
|
|
209
|
+
|
|
210
|
+
if figure_info["status"] == "completed":
|
|
211
|
+
print(f"Figure already exists at: {figure_url}")
|
|
212
|
+
return figure_url
|
|
213
|
+
|
|
214
|
+
print(f"Using figure URL: {figure_url}")
|
|
215
|
+
|
|
216
|
+
files_to_upload = all_files
|
|
217
|
+
total_files_to_upload = len(files_to_upload)
|
|
218
|
+
|
|
219
|
+
if total_files_to_upload == 0:
|
|
220
|
+
print("No files to upload")
|
|
221
|
+
else:
|
|
222
|
+
print(
|
|
223
|
+
f"Uploading {total_files_to_upload} files with up to {MAX_WORKERS_FOR_UPLOAD} concurrent uploads..."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Thread-safe progress tracking
|
|
227
|
+
uploaded_count = 0
|
|
228
|
+
count_lock = threading.Lock()
|
|
229
|
+
|
|
230
|
+
# Upload files in parallel with concurrent uploads
|
|
231
|
+
with ThreadPoolExecutor(max_workers=MAX_WORKERS_FOR_UPLOAD) as executor:
|
|
232
|
+
# Submit all upload tasks
|
|
233
|
+
future_to_file = {
|
|
234
|
+
executor.submit(
|
|
235
|
+
_upload_single_file, figure_url, rel_path, file_path, api_key
|
|
236
|
+
): rel_path
|
|
237
|
+
for rel_path, file_path in files_to_upload
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Process completed uploads
|
|
241
|
+
for future in as_completed(future_to_file):
|
|
242
|
+
relative_path = future_to_file[future]
|
|
243
|
+
try:
|
|
244
|
+
future.result() # This will raise any exception that occurred during upload
|
|
245
|
+
|
|
246
|
+
# Thread-safe progress update
|
|
247
|
+
with count_lock:
|
|
248
|
+
uploaded_count += 1
|
|
249
|
+
print(
|
|
250
|
+
f"Uploaded {uploaded_count}/{total_files_to_upload}: {relative_path}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
print(f"Failed to upload {relative_path}: {e}")
|
|
255
|
+
raise # Re-raise the exception to stop the upload process
|
|
256
|
+
|
|
257
|
+
# Create manifest for finalization
|
|
258
|
+
print("Creating manifest...")
|
|
259
|
+
manifest = {
|
|
260
|
+
"timestamp": time.time(),
|
|
261
|
+
"files": [],
|
|
262
|
+
"total_size": 0,
|
|
263
|
+
"total_files": len(files_to_upload),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for rel_path, file_path in files_to_upload:
|
|
267
|
+
file_size = file_path.stat().st_size
|
|
268
|
+
manifest["files"].append({"path": rel_path, "size": file_size})
|
|
269
|
+
manifest["total_size"] += file_size
|
|
270
|
+
|
|
271
|
+
print(f"Total size: {manifest['total_size'] / (1024 * 1024):.2f} MB")
|
|
272
|
+
|
|
273
|
+
# Upload manifest.json
|
|
274
|
+
print("Uploading manifest.json...")
|
|
275
|
+
manifest_content = json.dumps(manifest, indent=2)
|
|
276
|
+
manifest_size = len(manifest_content.encode("utf-8"))
|
|
277
|
+
|
|
278
|
+
manifest_payload = {
|
|
279
|
+
"figureUrl": figure_url,
|
|
280
|
+
"relativePath": "manifest.json",
|
|
281
|
+
"apiKey": api_key,
|
|
282
|
+
"size": manifest_size,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
response = requests.post(
|
|
286
|
+
f"{FIGPACK_API_BASE_URL}/api/upload", json=manifest_payload
|
|
287
|
+
)
|
|
288
|
+
if not response.ok:
|
|
289
|
+
try:
|
|
290
|
+
error_data = response.json()
|
|
291
|
+
error_msg = error_data.get("message", "Unknown error")
|
|
292
|
+
except:
|
|
293
|
+
error_msg = f"HTTP {response.status_code}"
|
|
294
|
+
raise Exception(f"Failed to get signed URL for manifest.json: {error_msg}")
|
|
295
|
+
|
|
296
|
+
response_data = response.json()
|
|
297
|
+
if not response_data.get("success"):
|
|
298
|
+
raise Exception(
|
|
299
|
+
f"Failed to get signed URL for manifest.json: {response_data.get('message', 'Unknown error')}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
signed_url = response_data.get("signedUrl")
|
|
303
|
+
if not signed_url:
|
|
304
|
+
raise Exception("No signed URL returned for manifest.json")
|
|
305
|
+
|
|
306
|
+
# Upload manifest using signed URL
|
|
307
|
+
upload_response = requests.put(
|
|
308
|
+
signed_url, data=manifest_content, headers={"Content-Type": "application/json"}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if not upload_response.ok:
|
|
312
|
+
raise Exception(
|
|
313
|
+
f"Failed to upload manifest.json to signed URL: HTTP {upload_response.status_code}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Finalize the figure upload
|
|
317
|
+
print("Finalizing figure...")
|
|
318
|
+
_finalize_figure(figure_url, api_key)
|
|
319
|
+
print("Upload completed successfully")
|
|
320
|
+
|
|
321
|
+
return figure_url
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _determine_content_type(file_path: str) -> str:
|
|
325
|
+
"""
|
|
326
|
+
Determine content type for upload based on file extension
|
|
327
|
+
"""
|
|
328
|
+
file_name = file_path.split("/")[-1]
|
|
329
|
+
extension = file_name.split(".")[-1] if "." in file_name else ""
|
|
330
|
+
|
|
331
|
+
content_type_map = {
|
|
332
|
+
"json": "application/json",
|
|
333
|
+
"html": "text/html",
|
|
334
|
+
"css": "text/css",
|
|
335
|
+
"js": "application/javascript",
|
|
336
|
+
"png": "image/png",
|
|
337
|
+
"zattrs": "application/json",
|
|
338
|
+
"zgroup": "application/json",
|
|
339
|
+
"zarray": "application/json",
|
|
340
|
+
"zmetadata": "application/json",
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return content_type_map.get(extension, "application/octet-stream")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
FIGPACK_API_BASE_URL = "https://figpack-api.vercel.app"
|