figpack 0.1.5__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.

Files changed (75) hide show
  1. {figpack-0.1.5/figpack.egg-info → figpack-0.1.6}/PKG-INFO +2 -2
  2. {figpack-0.1.5 → figpack-0.1.6}/README.md +1 -1
  3. {figpack-0.1.5 → figpack-0.1.6}/figpack/__init__.py +1 -1
  4. {figpack-0.1.5 → figpack-0.1.6}/figpack/core/_show_view.py +4 -4
  5. figpack-0.1.6/figpack/core/_upload_bundle.py +343 -0
  6. figpack-0.1.6/figpack/core/config.py +1 -0
  7. figpack-0.1.5/figpack/figpack-gui-dist/assets/index-DeyVLaXh.js → figpack-0.1.6/figpack/figpack-gui-dist/assets/index-DaeClgi6.js +1 -1
  8. {figpack-0.1.5 → figpack-0.1.6}/figpack/figpack-gui-dist/index.html +1 -1
  9. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/Box.py +6 -0
  10. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/Image.py +8 -0
  11. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/MultiChannelTimeseries.py +6 -2
  12. {figpack-0.1.5 → figpack-0.1.6/figpack.egg-info}/PKG-INFO +2 -2
  13. {figpack-0.1.5 → figpack-0.1.6}/figpack.egg-info/SOURCES.txt +11 -6
  14. {figpack-0.1.5 → figpack-0.1.6}/pyproject.toml +1 -1
  15. figpack-0.1.6/tests/test_box.py +62 -0
  16. figpack-0.1.6/tests/test_cli.py +275 -0
  17. figpack-0.1.6/tests/test_core.py +62 -0
  18. figpack-0.1.6/tests/test_figpack_view.py +106 -0
  19. figpack-0.1.6/tests/test_image.py +60 -0
  20. figpack-0.1.6/tests/test_markdown.py +65 -0
  21. figpack-0.1.6/tests/test_matplotlib_figure.py +111 -0
  22. figpack-0.1.6/tests/test_multichannel_timeseries.py +221 -0
  23. figpack-0.1.6/tests/test_plotly_figure.py +124 -0
  24. figpack-0.1.6/tests/test_show_view.py +521 -0
  25. figpack-0.1.6/tests/test_spike_sorting_correlograms.py +133 -0
  26. figpack-0.1.6/tests/test_splitter.py +108 -0
  27. figpack-0.1.6/tests/test_tablayout.py +150 -0
  28. figpack-0.1.6/tests/test_timeseries_graph.py +275 -0
  29. figpack-0.1.6/tests/test_units_table.py +172 -0
  30. figpack-0.1.6/tests/test_upload_bundle.py +679 -0
  31. figpack-0.1.5/figpack/core/_upload_bundle.py +0 -453
  32. figpack-0.1.5/tests/test_cli.py +0 -306
  33. figpack-0.1.5/tests/test_matplotlib_figure.py +0 -181
  34. figpack-0.1.5/tests/test_multichannel_timeseries.py +0 -118
  35. figpack-0.1.5/tests/test_package.py +0 -51
  36. figpack-0.1.5/tests/test_plotly_figure.py +0 -304
  37. figpack-0.1.5/tests/test_show_view.py +0 -433
  38. figpack-0.1.5/tests/test_si_autocorrelograms.py +0 -60
  39. figpack-0.1.5/tests/test_si_cross_correlograms.py +0 -44
  40. figpack-0.1.5/tests/test_splitter.py +0 -362
  41. figpack-0.1.5/tests/test_timeseries_graph.py +0 -583
  42. figpack-0.1.5/tests/test_upload_bundle.py +0 -629
  43. figpack-0.1.5/tests/test_views.py +0 -244
  44. {figpack-0.1.5 → figpack-0.1.6}/LICENSE +0 -0
  45. {figpack-0.1.5 → figpack-0.1.6}/MANIFEST.in +0 -0
  46. {figpack-0.1.5 → figpack-0.1.6}/figpack/cli.py +0 -0
  47. {figpack-0.1.5 → figpack-0.1.6}/figpack/core/__init__.py +0 -0
  48. {figpack-0.1.5 → figpack-0.1.6}/figpack/core/_bundle_utils.py +0 -0
  49. {figpack-0.1.5 → figpack-0.1.6}/figpack/core/figpack_view.py +0 -0
  50. {figpack-0.1.5 → figpack-0.1.6}/figpack/figpack-gui-dist/assets/index-BDa2iJW9.css +0 -0
  51. {figpack-0.1.5 → figpack-0.1.6}/figpack/figpack-gui-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
  52. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/__init__.py +0 -0
  53. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/views/AutocorrelogramItem.py +0 -0
  54. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/views/Autocorrelograms.py +0 -0
  55. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/views/CrossCorrelogramItem.py +0 -0
  56. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/views/CrossCorrelograms.py +0 -0
  57. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/views/UnitSimilarityScore.py +0 -0
  58. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/views/UnitsTable.py +0 -0
  59. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/views/UnitsTableColumn.py +0 -0
  60. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/views/UnitsTableRow.py +0 -0
  61. {figpack-0.1.5 → figpack-0.1.6}/figpack/spike_sorting/views/__init__.py +0 -0
  62. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/LayoutItem.py +0 -0
  63. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/Markdown.py +0 -0
  64. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/MatplotlibFigure.py +0 -0
  65. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/PlotlyFigure.py +0 -0
  66. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/Splitter.py +0 -0
  67. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/TabLayout.py +0 -0
  68. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/TabLayoutItem.py +0 -0
  69. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/TimeseriesGraph.py +0 -0
  70. {figpack-0.1.5 → figpack-0.1.6}/figpack/views/__init__.py +0 -0
  71. {figpack-0.1.5 → figpack-0.1.6}/figpack.egg-info/dependency_links.txt +0 -0
  72. {figpack-0.1.5 → figpack-0.1.6}/figpack.egg-info/entry_points.txt +0 -0
  73. {figpack-0.1.5 → figpack-0.1.6}/figpack.egg-info/requires.txt +0 -0
  74. {figpack-0.1.5 → figpack-0.1.6}/figpack.egg-info/top_level.txt +0 -0
  75. {figpack-0.1.5 → 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.5
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
@@ -126,7 +126,7 @@ view.show(open_in_browser=True, title="Local Visualization")
126
126
 
127
127
  ### Sharing Online
128
128
 
129
- Set the `FIGPACK_UPLOAD_PASSCODE` environment variable and use:
129
+ Set the `FIGPACK_API_KEY` environment variable and use:
130
130
 
131
131
  ```python
132
132
  view.show(upload=True, open_in_browser=True, title="Shared Visualization")
@@ -82,7 +82,7 @@ view.show(open_in_browser=True, title="Local Visualization")
82
82
 
83
83
  ### Sharing Online
84
84
 
85
- Set the `FIGPACK_UPLOAD_PASSCODE` environment variable and use:
85
+ Set the `FIGPACK_API_KEY` environment variable and use:
86
86
 
87
87
  ```python
88
88
  view.show(upload=True, open_in_browser=True, title="Shared Visualization")
@@ -2,4 +2,4 @@
2
2
  figpack - A Python package for creating shareable, interactive visualizations in the browser
3
3
  """
4
4
 
5
- __version__ = "0.1.5"
5
+ __version__ = "0.1.6"
@@ -28,15 +28,15 @@ def _show_view(
28
28
 
29
29
  if upload:
30
30
  # Check for required environment variable
31
- passcode = os.environ.get("FIGPACK_UPLOAD_PASSCODE")
32
- if not passcode:
31
+ api_key = os.environ.get("FIGPACK_API_KEY")
32
+ if not api_key:
33
33
  raise EnvironmentError(
34
- "FIGPACK_UPLOAD_PASSCODE environment variable must be set to upload views."
34
+ "FIGPACK_API_KEY environment variable must be set to upload views."
35
35
  )
36
36
 
37
37
  # Upload the bundle
38
38
  print("Starting upload...")
39
- figure_url = _upload_bundle(tmpdir, passcode)
39
+ figure_url = _upload_bundle(tmpdir, api_key)
40
40
 
41
41
  if open_in_browser:
42
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"