figpack 0.1.5__tar.gz → 0.1.7__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.7}/PKG-INFO +2 -2
  2. {figpack-0.1.5 → figpack-0.1.7}/README.md +1 -1
  3. {figpack-0.1.5 → figpack-0.1.7}/figpack/__init__.py +1 -1
  4. {figpack-0.1.5 → figpack-0.1.7}/figpack/core/_show_view.py +4 -4
  5. figpack-0.1.7/figpack/core/_upload_bundle.py +358 -0
  6. figpack-0.1.7/figpack/core/config.py +5 -0
  7. figpack-0.1.5/figpack/figpack-gui-dist/assets/index-DeyVLaXh.js → figpack-0.1.7/figpack/figpack-gui-dist/assets/index-DaeClgi6.js +1 -1
  8. {figpack-0.1.5 → figpack-0.1.7}/figpack/figpack-gui-dist/index.html +1 -1
  9. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/Box.py +6 -0
  10. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/Image.py +8 -0
  11. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/MultiChannelTimeseries.py +6 -2
  12. {figpack-0.1.5 → figpack-0.1.7/figpack.egg-info}/PKG-INFO +2 -2
  13. {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/SOURCES.txt +11 -6
  14. {figpack-0.1.5 → figpack-0.1.7}/pyproject.toml +1 -1
  15. figpack-0.1.7/tests/test_box.py +62 -0
  16. figpack-0.1.7/tests/test_cli.py +275 -0
  17. figpack-0.1.7/tests/test_core.py +62 -0
  18. figpack-0.1.7/tests/test_figpack_view.py +106 -0
  19. figpack-0.1.7/tests/test_image.py +60 -0
  20. figpack-0.1.7/tests/test_markdown.py +65 -0
  21. figpack-0.1.7/tests/test_matplotlib_figure.py +111 -0
  22. figpack-0.1.7/tests/test_multichannel_timeseries.py +221 -0
  23. figpack-0.1.7/tests/test_plotly_figure.py +124 -0
  24. figpack-0.1.7/tests/test_show_view.py +521 -0
  25. figpack-0.1.7/tests/test_spike_sorting_correlograms.py +133 -0
  26. figpack-0.1.7/tests/test_splitter.py +108 -0
  27. figpack-0.1.7/tests/test_tablayout.py +150 -0
  28. figpack-0.1.7/tests/test_timeseries_graph.py +275 -0
  29. figpack-0.1.7/tests/test_units_table.py +172 -0
  30. figpack-0.1.7/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.7}/LICENSE +0 -0
  45. {figpack-0.1.5 → figpack-0.1.7}/MANIFEST.in +0 -0
  46. {figpack-0.1.5 → figpack-0.1.7}/figpack/cli.py +0 -0
  47. {figpack-0.1.5 → figpack-0.1.7}/figpack/core/__init__.py +0 -0
  48. {figpack-0.1.5 → figpack-0.1.7}/figpack/core/_bundle_utils.py +0 -0
  49. {figpack-0.1.5 → figpack-0.1.7}/figpack/core/figpack_view.py +0 -0
  50. {figpack-0.1.5 → figpack-0.1.7}/figpack/figpack-gui-dist/assets/index-BDa2iJW9.css +0 -0
  51. {figpack-0.1.5 → figpack-0.1.7}/figpack/figpack-gui-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
  52. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/__init__.py +0 -0
  53. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/AutocorrelogramItem.py +0 -0
  54. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/Autocorrelograms.py +0 -0
  55. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/CrossCorrelogramItem.py +0 -0
  56. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/CrossCorrelograms.py +0 -0
  57. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/UnitSimilarityScore.py +0 -0
  58. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/UnitsTable.py +0 -0
  59. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/UnitsTableColumn.py +0 -0
  60. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/UnitsTableRow.py +0 -0
  61. {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/__init__.py +0 -0
  62. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/LayoutItem.py +0 -0
  63. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/Markdown.py +0 -0
  64. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/MatplotlibFigure.py +0 -0
  65. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/PlotlyFigure.py +0 -0
  66. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/Splitter.py +0 -0
  67. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/TabLayout.py +0 -0
  68. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/TabLayoutItem.py +0 -0
  69. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/TimeseriesGraph.py +0 -0
  70. {figpack-0.1.5 → figpack-0.1.7}/figpack/views/__init__.py +0 -0
  71. {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/dependency_links.txt +0 -0
  72. {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/entry_points.txt +0 -0
  73. {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/requires.txt +0 -0
  74. {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/top_level.txt +0 -0
  75. {figpack-0.1.5 → figpack-0.1.7}/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.7
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.7"
@@ -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, title=title)
40
40
 
41
41
  if open_in_browser:
42
42
  webbrowser.open(figure_url)
@@ -0,0 +1,358 @@
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,
110
+ api_key: str,
111
+ total_files: int = None,
112
+ total_size: int = None,
113
+ title: str = None,
114
+ ) -> dict:
115
+ """
116
+ Create a new figure or get existing figure information
117
+
118
+ Args:
119
+ figure_hash: The hash of the figure
120
+ api_key: The API key for authentication
121
+ total_files: Optional total number of files
122
+ total_size: Optional total size of files
123
+ title: Optional title for the figure
124
+
125
+ Returns:
126
+ dict: Figure information from the API
127
+ """
128
+ payload = {
129
+ "figureHash": figure_hash,
130
+ "apiKey": api_key,
131
+ "figpackVersion": __version__,
132
+ }
133
+
134
+ if total_files is not None:
135
+ payload["totalFiles"] = total_files
136
+ if total_size is not None:
137
+ payload["totalSize"] = total_size
138
+ if title is not None:
139
+ payload["title"] = title
140
+
141
+ response = requests.post(f"{FIGPACK_API_BASE_URL}/api/figures/create", json=payload)
142
+
143
+ if not response.ok:
144
+ try:
145
+ error_data = response.json()
146
+ error_msg = error_data.get("message", "Unknown error")
147
+ except:
148
+ error_msg = f"HTTP {response.status_code}"
149
+ raise Exception(f"Failed to create figure {figure_hash}: {error_msg}")
150
+
151
+ response_data = response.json()
152
+ if not response_data.get("success"):
153
+ raise Exception(
154
+ f"Failed to create figure {figure_hash}: {response_data.get('message', 'Unknown error')}"
155
+ )
156
+
157
+ return response_data
158
+
159
+
160
+ def _finalize_figure(figure_url: str, api_key: str) -> dict:
161
+ """
162
+ Finalize a figure upload
163
+
164
+ Returns:
165
+ dict: Figure information from the API
166
+ """
167
+ payload = {
168
+ "figureUrl": figure_url,
169
+ "apiKey": api_key,
170
+ }
171
+
172
+ response = requests.post(
173
+ f"{FIGPACK_API_BASE_URL}/api/figures/finalize", json=payload
174
+ )
175
+
176
+ if not response.ok:
177
+ try:
178
+ error_data = response.json()
179
+ error_msg = error_data.get("message", "Unknown error")
180
+ except:
181
+ error_msg = f"HTTP {response.status_code}"
182
+ raise Exception(f"Failed to finalize figure {figure_url}: {error_msg}")
183
+
184
+ response_data = response.json()
185
+ if not response_data.get("success"):
186
+ raise Exception(
187
+ f"Failed to finalize figure {figure_url}: {response_data.get('message', 'Unknown error')}"
188
+ )
189
+
190
+ return response_data
191
+
192
+
193
+ def _upload_bundle(tmpdir: str, api_key: str, title: str = None) -> str:
194
+ """
195
+ Upload the prepared bundle to the cloud using the new database-driven approach
196
+ """
197
+ tmpdir_path = pathlib.Path(tmpdir)
198
+
199
+ # Compute deterministic figure ID based on file contents
200
+ print("Computing deterministic figure ID...")
201
+ figure_hash = _compute_deterministic_figure_hash(tmpdir_path)
202
+ print(f"Figure hash: {figure_hash}")
203
+
204
+ # Collect all files to upload
205
+ all_files = []
206
+ for file_path in tmpdir_path.rglob("*"):
207
+ if file_path.is_file():
208
+ relative_path = file_path.relative_to(tmpdir_path)
209
+ all_files.append((str(relative_path), file_path))
210
+
211
+ # Calculate total files and size for metadata
212
+ total_files = len(all_files)
213
+ total_size = sum(file_path.stat().st_size for _, file_path in all_files)
214
+ print(
215
+ f"Found {total_files} files to upload, total size: {total_size / (1024 * 1024):.2f} MB"
216
+ )
217
+
218
+ # Find available figure ID and create/get figure in database with metadata
219
+ result = _create_or_get_figure(
220
+ figure_hash, api_key, total_files, total_size, title=title
221
+ )
222
+ figure_info = result.get("figure", {})
223
+ figure_url = figure_info.get("figureUrl")
224
+
225
+ if figure_info["status"] == "completed":
226
+ print(f"Figure already exists at: {figure_url}")
227
+ return figure_url
228
+
229
+ print(f"Using figure URL: {figure_url}")
230
+
231
+ files_to_upload = all_files
232
+ total_files_to_upload = len(files_to_upload)
233
+
234
+ if total_files_to_upload == 0:
235
+ print("No files to upload")
236
+ else:
237
+ print(
238
+ f"Uploading {total_files_to_upload} files with up to {MAX_WORKERS_FOR_UPLOAD} concurrent uploads..."
239
+ )
240
+
241
+ # Thread-safe progress tracking
242
+ uploaded_count = 0
243
+ count_lock = threading.Lock()
244
+
245
+ # Upload files in parallel with concurrent uploads
246
+ with ThreadPoolExecutor(max_workers=MAX_WORKERS_FOR_UPLOAD) as executor:
247
+ # Submit all upload tasks
248
+ future_to_file = {
249
+ executor.submit(
250
+ _upload_single_file, figure_url, rel_path, file_path, api_key
251
+ ): rel_path
252
+ for rel_path, file_path in files_to_upload
253
+ }
254
+
255
+ # Process completed uploads
256
+ for future in as_completed(future_to_file):
257
+ relative_path = future_to_file[future]
258
+ try:
259
+ future.result() # This will raise any exception that occurred during upload
260
+
261
+ # Thread-safe progress update
262
+ with count_lock:
263
+ uploaded_count += 1
264
+ print(
265
+ f"Uploaded {uploaded_count}/{total_files_to_upload}: {relative_path}"
266
+ )
267
+
268
+ except Exception as e:
269
+ print(f"Failed to upload {relative_path}: {e}")
270
+ raise # Re-raise the exception to stop the upload process
271
+
272
+ # Create manifest for finalization
273
+ print("Creating manifest...")
274
+ manifest = {
275
+ "timestamp": time.time(),
276
+ "files": [],
277
+ "total_size": 0,
278
+ "total_files": len(files_to_upload),
279
+ }
280
+
281
+ for rel_path, file_path in files_to_upload:
282
+ file_size = file_path.stat().st_size
283
+ manifest["files"].append({"path": rel_path, "size": file_size})
284
+ manifest["total_size"] += file_size
285
+
286
+ print(f"Total size: {manifest['total_size'] / (1024 * 1024):.2f} MB")
287
+
288
+ # Upload manifest.json
289
+ print("Uploading manifest.json...")
290
+ manifest_content = json.dumps(manifest, indent=2)
291
+ manifest_size = len(manifest_content.encode("utf-8"))
292
+
293
+ manifest_payload = {
294
+ "figureUrl": figure_url,
295
+ "relativePath": "manifest.json",
296
+ "apiKey": api_key,
297
+ "size": manifest_size,
298
+ }
299
+
300
+ response = requests.post(
301
+ f"{FIGPACK_API_BASE_URL}/api/upload", json=manifest_payload
302
+ )
303
+ if not response.ok:
304
+ try:
305
+ error_data = response.json()
306
+ error_msg = error_data.get("message", "Unknown error")
307
+ except:
308
+ error_msg = f"HTTP {response.status_code}"
309
+ raise Exception(f"Failed to get signed URL for manifest.json: {error_msg}")
310
+
311
+ response_data = response.json()
312
+ if not response_data.get("success"):
313
+ raise Exception(
314
+ f"Failed to get signed URL for manifest.json: {response_data.get('message', 'Unknown error')}"
315
+ )
316
+
317
+ signed_url = response_data.get("signedUrl")
318
+ if not signed_url:
319
+ raise Exception("No signed URL returned for manifest.json")
320
+
321
+ # Upload manifest using signed URL
322
+ upload_response = requests.put(
323
+ signed_url, data=manifest_content, headers={"Content-Type": "application/json"}
324
+ )
325
+
326
+ if not upload_response.ok:
327
+ raise Exception(
328
+ f"Failed to upload manifest.json to signed URL: HTTP {upload_response.status_code}"
329
+ )
330
+
331
+ # Finalize the figure upload
332
+ print("Finalizing figure...")
333
+ _finalize_figure(figure_url, api_key)
334
+ print("Upload completed successfully")
335
+
336
+ return figure_url
337
+
338
+
339
+ def _determine_content_type(file_path: str) -> str:
340
+ """
341
+ Determine content type for upload based on file extension
342
+ """
343
+ file_name = file_path.split("/")[-1]
344
+ extension = file_name.split(".")[-1] if "." in file_name else ""
345
+
346
+ content_type_map = {
347
+ "json": "application/json",
348
+ "html": "text/html",
349
+ "css": "text/css",
350
+ "js": "application/javascript",
351
+ "png": "image/png",
352
+ "zattrs": "application/json",
353
+ "zgroup": "application/json",
354
+ "zarray": "application/json",
355
+ "zmetadata": "application/json",
356
+ }
357
+
358
+ return content_type_map.get(extension, "application/octet-stream")
@@ -0,0 +1,5 @@
1
+ import os
2
+
3
+ FIGPACK_API_BASE_URL = os.getenv(
4
+ "FIGPACK_API_BASE_URL", "https://figpack-api.vercel.app"
5
+ )