arrayview 0.35.2__py3-none-any.whl → 0.36.0__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.
arrayview/_launcher.py CHANGED
@@ -2590,6 +2590,11 @@ _PERSIST_DAEMON_CONNECT_TIMEOUT_SECONDS = float(
2590
2590
  _PERSIST_DAEMON_IDLE_SECONDS = float(
2591
2591
  os.environ.get("ARRAYVIEW_PERSIST_IDLE_SECONDS", "1800")
2592
2592
  )
2593
+ # How long a daemon whose initial load failed keeps serving so the opener can
2594
+ # read the reason from /metadata/<sid> before the port is released.
2595
+ _FAILED_LOAD_EXIT_GRACE_SECONDS = float(
2596
+ os.environ.get("ARRAYVIEW_FAILED_LOAD_EXIT_GRACE_SECONDS", "30")
2597
+ )
2593
2598
 
2594
2599
 
2595
2600
  def _join_query_values(values: _CSVValues) -> str:
@@ -4169,6 +4174,7 @@ def _serve_daemon(
4169
4174
  owner="persistent" if persist else "transient",
4170
4175
  )
4171
4176
  # Register sid as pending so /metadata can poll while data loads.
4177
+ _initial_load_failed = threading.Event()
4172
4178
  _session_mod.PENDING_SESSIONS.add(sid)
4173
4179
  _pending_event = threading.Event()
4174
4180
  _session_mod.PENDING_SESSION_EVENTS[sid] = _pending_event
@@ -4349,6 +4355,18 @@ def _serve_daemon(
4349
4355
  sid_tag=sid_tag,
4350
4356
  error_type=type(exc).__name__,
4351
4357
  )
4358
+ # This runs on a daemon thread, so re-raising only kills the thread
4359
+ # and writes to a stderr nobody reads. Record why the sid will never
4360
+ # become ready so /metadata can report it instead of returning 404
4361
+ # until the opener gives up.
4362
+ _session_mod.FAILED_PENDING_SESSIONS[sid] = (
4363
+ str(exc) or type(exc).__name__
4364
+ )
4365
+ _vprint(
4366
+ f"[ArrayView] Failed to load {filepath}: {exc}",
4367
+ flush=True,
4368
+ )
4369
+ _initial_load_failed.set()
4352
4370
  raise
4353
4371
  finally:
4354
4372
  _session_mod.PENDING_SESSIONS.discard(sid)
@@ -4357,6 +4375,32 @@ def _serve_daemon(
4357
4375
 
4358
4376
  threading.Thread(target=_load, daemon=True).start()
4359
4377
 
4378
+ def _exit_if_initial_load_failed() -> None:
4379
+ """Release the port when the file this daemon exists to serve won't load.
4380
+
4381
+ uvicorn starts before the load finishes, so a failed load otherwise
4382
+ leaves a server with no session holding the port until the connect
4383
+ timeout expires minutes later — and answering /ping the whole time. The
4384
+ grace period is only so the opener can read the failure from
4385
+ /metadata/<sid> before the port goes away.
4386
+ """
4387
+ _initial_load_failed.wait()
4388
+ time.sleep(_FAILED_LOAD_EXIT_GRACE_SECONDS)
4389
+ if _session_mod.SESSIONS or _session_mod.VIEWER_SOCKETS:
4390
+ return # something else is using this server; leave it alone
4391
+ _trace_launch_event(
4392
+ "daemon.exiting",
4393
+ reason="initial_load_failed",
4394
+ instance_tag=_launch_trace_tag(record.instance_id),
4395
+ )
4396
+ try:
4397
+ registry.remove(record.instance_id)
4398
+ except Exception:
4399
+ pass
4400
+ os._exit(0)
4401
+
4402
+ threading.Thread(target=_exit_if_initial_load_failed, daemon=True).start()
4403
+
4360
4404
  if persist:
4361
4405
  _wait_for_viewer_close(
4362
4406
  idle_seconds=_PERSIST_DAEMON_IDLE_SECONDS,
@@ -18,6 +18,7 @@ from arrayview._lifecycle import (
18
18
  )
19
19
  from arrayview._session import (
20
20
  CANCELLED_PENDING_SESSIONS,
21
+ FAILED_PENDING_SESSIONS,
21
22
  PENDING_SESSION_EVENTS,
22
23
  PENDING_SESSIONS,
23
24
  SESSIONS,
@@ -263,6 +264,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
263
264
  # the real Session instead of seeing a transient 404.
264
265
  sid = requested_sid or uuid.uuid4().hex
265
266
  pending_event = threading.Event()
267
+ FAILED_PENDING_SESSIONS.pop(sid, None)
266
268
  PENDING_SESSIONS.add(sid)
267
269
  PENDING_SESSION_EVENTS[sid] = pending_event
268
270
  signature_before_load = file_signature(abs_path)
@@ -293,12 +295,14 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
293
295
  ]
294
296
  commit_pending_session(sid, session)
295
297
  except Exception as exc:
296
- # Keep the server alive and make the failure visible in its
297
- # log. The opener will fail closed when metadata never
298
- # becomes ready, while a retry creates a fresh request.
298
+ # Keep the server alive, and record why this sid will never
299
+ # become ready. Without the record the opener cannot tell a
300
+ # failed load from a slow one and burns its whole deadline
301
+ # on a file that raised immediately.
299
302
  import traceback
300
303
 
301
304
  traceback.print_exc()
305
+ FAILED_PENDING_SESSIONS[sid] = str(exc) or exc.__class__.__name__
302
306
  finally:
303
307
  PENDING_SESSIONS.discard(sid)
304
308
  CANCELLED_PENDING_SESSIONS.discard(sid)
@@ -211,6 +211,14 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
211
211
  # request; the opener retries until the pending Session is published.
212
212
  if sid in _session_mod.PENDING_SESSIONS:
213
213
  return Response(status_code=404, headers={"Retry-After": "1"})
214
+ # A load that raised is terminal. Say so with a distinct status and the
215
+ # real reason, so the caller stops polling and can show the user why
216
+ # this file will never open instead of waiting out its deadline.
217
+ load_error = _session_mod.FAILED_PENDING_SESSIONS.get(sid)
218
+ if load_error is not None:
219
+ return Response(
220
+ status_code=422, content=load_error.encode(), media_type="text/plain"
221
+ )
214
222
  session = await wait_for_session_ready(sid)
215
223
  if not session:
216
224
  return Response(status_code=404)
arrayview/_session.py CHANGED
@@ -41,6 +41,10 @@ _window_process = None
41
41
  PENDING_SESSIONS: set = set() # sids whose data is still loading in a background thread
42
42
  PENDING_SESSION_EVENTS: dict[str, threading.Event] = {}
43
43
  CANCELLED_PENDING_SESSIONS: set[str] = set()
44
+ # sid -> why its background load failed. A load that raises must be reportable:
45
+ # without this the opener cannot tell "still loading" from "will never load" and
46
+ # waits out its whole deadline on a file that failed in the first second.
47
+ FAILED_PENDING_SESSIONS: dict[str, str] = {}
44
48
 
45
49
 
46
50
  def file_signature(filepath: str):
@@ -575,6 +579,7 @@ __all__ = [
575
579
  "_window_process",
576
580
  "PENDING_SESSIONS",
577
581
  "PENDING_SESSION_EVENTS",
582
+ "FAILED_PENDING_SESSIONS",
578
583
  "wait_for_session_ready",
579
584
  # Render thread pool
580
585
  "_RENDER_QUEUE",
arrayview/_viewer.html CHANGED
@@ -9278,7 +9278,20 @@
9278
9278
  function formatValueGroup(values, options = {}) {
9279
9279
  const finite = (Array.isArray(values) ? values : [values]).filter(Number.isFinite);
9280
9280
  const cap = Math.max(0, Math.min(8, Math.trunc(options.maxDecimals ?? 7)));
9281
- const minDecimals = Math.max(0, Math.min(cap, Math.trunc(options.minDecimals ?? 2)));
9281
+ // Decimals worth showing depend on magnitude. Two was applied at
9282
+ // every scale, so a range topping out near 1890 read as "1890.04"
9283
+ // when the hundredths carry no information. The floor is only ever
9284
+ // lowered for large values, never raised, so small ranges keep the
9285
+ // precision they had. The search below can still add decimals when
9286
+ // values would otherwise collide.
9287
+ const _defaultMinDecimals = (vals) => {
9288
+ const m = Math.max(...vals.filter(Number.isFinite).map(v => Math.abs(v)));
9289
+ if (!(m > 0) || !Number.isFinite(m)) return 2;
9290
+ return Math.max(0, Math.min(2, 3 - Math.floor(Math.log10(m))));
9291
+ };
9292
+ const minDecimals = Math.max(0, Math.min(cap, Math.trunc(
9293
+ options.minDecimals ?? _defaultMinDecimals(finite)
9294
+ )));
9282
9295
  const ordinaryLo = options.ordinaryLo ?? 1e-4;
9283
9296
  const ordinaryHi = options.ordinaryHi ?? 1e6;
9284
9297
  const fallback = {
@@ -14242,6 +14255,41 @@
14242
14255
 
14243
14256
  /* ── JS: Initialization and Metadata Fetch ────────────────── */
14244
14257
 
14258
+ // The backend serves this page as soon as its port is up, which can be
14259
+ // seconds before a large array has finished loading. While the session
14260
+ // is still loading /metadata answers 404 with a Retry-After header; an
14261
+ // unknown or released sid answers 404 without one, and a load that
14262
+ // failed answers 422 with the reason. Keep waiting only for the first.
14263
+ async function fetchMetadataWithRetry(sid, timeoutMs = 180000) {
14264
+ const deadline = Date.now() + timeoutMs;
14265
+ for (;;) {
14266
+ const res = await proxyFetch(`/metadata/${sid}`);
14267
+ if (res.ok) return { data: await res.json() };
14268
+ if (res.status === 422) {
14269
+ let reason = '';
14270
+ try { reason = (await res.text()).trim(); } catch (_e) {}
14271
+ return { error: reason || 'This file could not be loaded.', status: 422 };
14272
+ }
14273
+ const pending = res.status === 404 && res.headers.get('Retry-After');
14274
+ if (!pending || Date.now() >= deadline) {
14275
+ return { error: null, status: res.status };
14276
+ }
14277
+ await new Promise(r => setTimeout(r, 300));
14278
+ }
14279
+ }
14280
+
14281
+ function showMetadataError(result) {
14282
+ const msg = result.error
14283
+ ? result.error
14284
+ : (result.status === 404
14285
+ ? 'Session not found or expired.'
14286
+ : `Server error ${result.status} — check Python console for details.`);
14287
+ document.getElementById('loading-overlay').innerHTML =
14288
+ `<div style="color:#c55;font-size:16px;font-family:monospace;max-width:60ch;text-align:center;line-height:1.5;padding:0 2em">${
14289
+ msg.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
14290
+ }</div>`;
14291
+ }
14292
+
14245
14293
  async function init() {
14246
14294
  // Loading spinner is in static HTML — body.av-loading set by server template.
14247
14295
  if (!sid) { hideLoadingOverlay(); drawPlasmaDemo(); return; }
@@ -14260,26 +14308,24 @@
14260
14308
  );
14261
14309
  data = await Promise.race([_metadataFromWs, httpFallback]);
14262
14310
  if (!data) {
14263
- // Both failed — try direct HTTP without timeout
14264
- const res = await proxyFetch(`/metadata/${sid}`);
14265
- if (!res.ok) {
14266
- reportParent('metadata-http-error', { status: res.status, sid });
14267
- const msg = res.status === 404 ? 'Session not found or expired.' : `Server error ${res.status} — check Python console for details.`;
14268
- document.getElementById('loading-overlay').innerHTML = `<div style="color:#c55;font-size:18px;font-family:monospace">${msg}</div>`;
14311
+ // Both failed — wait out a still-loading session.
14312
+ const result = await fetchMetadataWithRetry(sid);
14313
+ if (!result.data) {
14314
+ reportParent('metadata-http-error', { status: result.status, sid });
14315
+ showMetadataError(result);
14269
14316
  return;
14270
14317
  }
14271
- data = await res.json();
14318
+ data = result.data;
14272
14319
  }
14273
14320
  } else {
14274
14321
  // HTTP transport mode (VS Code) — use traditional fetch
14275
- const res = await proxyFetch(`/metadata/${sid}`);
14276
- if (!res.ok) {
14277
- reportParent('metadata-http-error', { status: res.status, sid });
14278
- const msg = res.status === 404 ? 'Session not found or expired.' : `Server error ${res.status} — check Python console for details.`;
14279
- document.getElementById('loading-overlay').innerHTML = `<div style="color:#c55;font-size:18px;font-family:monospace">${msg}</div>`;
14322
+ const result = await fetchMetadataWithRetry(sid);
14323
+ if (!result.data) {
14324
+ reportParent('metadata-http-error', { status: result.status, sid });
14325
+ showMetadataError(result);
14280
14326
  return;
14281
14327
  }
14282
- data = await res.json();
14328
+ data = result.data;
14283
14329
  }
14284
14330
 
14285
14331
  await _loadOverlayLabelValues();
@@ -35919,9 +35965,12 @@
35919
35965
  return `${x.toFixed(1)},${y.toFixed(1)}`;
35920
35966
  }).join(' ');
35921
35967
  const distLabel = distance.toFixed(1) + ' px';
35968
+ // Share the colorbar's formatter so the same value does not read
35969
+ // differently here than it does on the colorbar.
35970
+ const _lpFmt = formatValueGroup([vMin, vMax]);
35922
35971
  const fmtVal = v => {
35923
35972
  if (v === null || !isFinite(v)) return '?';
35924
- return Math.abs(v) < 0.01 || Math.abs(v) >= 1e4 ? v.toExponential(2) : v.toFixed(2);
35973
+ return _lpFmt.format(v);
35925
35974
  };
35926
35975
  _lineProfileOverlay.innerHTML = `
35927
35976
  <svg width="${W}" height="${H}" xmlns="http://www.w3.org/2000/svg">
@@ -54,7 +54,7 @@ _VSCODE_EXT_INSTALLED = False # cached so we only check once per process
54
54
  _VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
55
55
  _VSCODE_EXT_RELOAD_REQUIRED = False # installed files are newer than the live host
56
56
  _VSCODE_EXT_INSTALL_FAILED = False # automatic install could not complete safely
57
- _VSCODE_EXT_VERSION = "0.14.76" # current bundled extension version
57
+ _VSCODE_EXT_VERSION = "0.14.87" # current bundled extension version
58
58
  _VSCODE_CONFIGURED_PORTS: set[int] = set()
59
59
 
60
60
  def _bundled_vscode_vsix_version(vsix_path: str) -> str | None:
@@ -633,6 +633,47 @@ def _ensure_vscode_extension(*, is_remote: bool | None = None) -> bool:
633
633
  # ---------------------------------------------------------------------------
634
634
 
635
635
 
636
+ def _port_has_listener(port: int) -> bool:
637
+ """Return True when something is listening on ``port`` on loopback."""
638
+ import socket
639
+
640
+ for family, addr in ((socket.AF_INET, "127.0.0.1"), (socket.AF_INET6, "::1")):
641
+ sock = socket.socket(family, socket.SOCK_STREAM)
642
+ try:
643
+ sock.settimeout(0.15)
644
+ if sock.connect_ex((addr, port)) == 0:
645
+ return True
646
+ except OSError:
647
+ continue
648
+ finally:
649
+ sock.close()
650
+ return False
651
+
652
+
653
+ def _stale_arrayview_port_keys(attrs: dict, keep_port: int) -> list[str]:
654
+ """Return ArrayView port entries that no longer have a live server.
655
+
656
+ Every launch on a fresh port used to leave its entry behind for good. On a
657
+ tunnel those entries keep telling VS Code to forward ports nothing serves,
658
+ which competes for a limited per-tunnel forwarding budget. Only entries
659
+ ArrayView wrote are considered, and only when their port is dead, so a
660
+ second concurrent ArrayView server is never disturbed.
661
+ """
662
+ stale: list[str] = []
663
+ for key, value in list(attrs.items()):
664
+ if not isinstance(value, dict) or value.get("label") != "ArrayView":
665
+ continue
666
+ if key == str(keep_port):
667
+ continue
668
+ try:
669
+ candidate = int(key)
670
+ except (TypeError, ValueError):
671
+ continue
672
+ if not _port_has_listener(candidate):
673
+ stale.append(key)
674
+ return stale
675
+
676
+
636
677
  def _configure_vscode_port_preview(
637
678
  port: int,
638
679
  *,
@@ -686,8 +727,11 @@ def _configure_vscode_port_preview(
686
727
  }
687
728
  current = attrs.get(str(port))
688
729
  updated = {**current, **desired} if isinstance(current, dict) else desired
689
- if current == updated:
730
+ stale = _stale_arrayview_port_keys(attrs, port)
731
+ if current == updated and not stale:
690
732
  return
733
+ for key in stale:
734
+ attrs.pop(key, None)
691
735
  attrs[str(port)] = updated
692
736
  with open(path, "w") as f:
693
737
  json.dump(settings, f, indent=2)
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.35.2
3
+ Version: 0.36.0
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -13,7 +13,7 @@ arrayview/_instance_registry.py,sha256=h7vyvCl0LTt9R8sDB1Vxe4a-KzfkLVuwrSw_ANpPT
13
13
  arrayview/_io.py,sha256=jJ4FQEHggSHCz2zi3ZQK4NjoJpBt3OkklQbM4_Jp1f8,61303
14
14
  arrayview/_launch_plan.py,sha256=UwmxmSZIFfutVZRhdlTZdrszCkxZMoSK99VvQHD-Mac,22192
15
15
  arrayview/_launch_trace.py,sha256=sKNIEDSpI8OgEC0aSpcWJPu0zdHFcKpQr-sPfsfiTz8,3711
16
- arrayview/_launcher.py,sha256=T455aghgzP329pmNMdVWIjreVPR3JGz9zkE8bf1nKTg,204399
16
+ arrayview/_launcher.py,sha256=ZBAvni9fgMW0Wj51RM1baDx3VH2jEaG2DP26A3OmFyU,206372
17
17
  arrayview/_lifecycle.py,sha256=SzLBjoDq7pj9jnq12oERmM9HA_2N4Y-CxwCRsfK4Q1s,3840
18
18
  arrayview/_overlays.py,sha256=TsMnXrP_BNSzCj4k8hbWNM-M5lcrro0xA_wusvAdLLw,4622
19
19
  arrayview/_platform.py,sha256=RCCHtOaj3hZIlzENfeLbqO0MRZu-OfiVHGu1-jhpMeY,26461
@@ -21,11 +21,11 @@ arrayview/_render.py,sha256=wVvwxQtZ7ldWYZTzlg2CS-esnPHlpSXMW_z22eh-rU0,30296
21
21
  arrayview/_routes_analysis.py,sha256=b2yiPm3qiapFPcBZWpltZrsTKNkFyHYV7o1O2ZrzVUo,32320
22
22
  arrayview/_routes_drop.py,sha256=FC4e7STdCo3hYIA0C4Yy9Ka2UKFx7nLDU5dkujmc1Vo,12477
23
23
  arrayview/_routes_export.py,sha256=Ha8mMO2E9TlRL5x25-ADV8_8xUiC-jRz9N1oWwgKOsg,3898
24
- arrayview/_routes_loading.py,sha256=PHXyXn-iJZyt35dPwRNf30_18UauylJBwbpysi0hzn0,33824
24
+ arrayview/_routes_loading.py,sha256=yYzf7NJeE8fLviCbAcXMSiAtSlLVY-wHwfdUqDOBCNA,34055
25
25
  arrayview/_routes_persistence.py,sha256=T03csHb7ppGT-8yMDf227XkqhnhjgSLYj0RW2wmg59g,17176
26
26
  arrayview/_routes_preferences.py,sha256=78i0OhwVJ9JF2RNazWecctU1-PI_PhNAv7bBIVxuYNU,4483
27
27
  arrayview/_routes_preload.py,sha256=KOsankNguxdIWmFW9iZcY9p2SvUN1mOKY7KkW1qMGcc,1729
28
- arrayview/_routes_query.py,sha256=FLf-IToSWrC7T8rfUnlbo2nWexgDOhvbooKIvF7m9iY,15364
28
+ arrayview/_routes_query.py,sha256=lzCoaz1fkRYC99QFBw0_6pLIlDEVNn99y_yH-q6DKzk,15825
29
29
  arrayview/_routes_rendering.py,sha256=OiYvdYE2PnTXVo9fUdWN1JvZ-VvUTUgGA7LYGrmMDSo,21688
30
30
  arrayview/_routes_segmentation.py,sha256=x2bBjQffZryeTMmOH38YdBWethw1o3KpA8V0chJcZHI,23599
31
31
  arrayview/_routes_state.py,sha256=8TyTURrJKV0taVYK7c9L-Dcx_At-fhHI1l0-4OQMpU0,11615
@@ -33,22 +33,22 @@ arrayview/_routes_vectorfield.py,sha256=QDUTG46cMEm-FjFWeIs2cv3b494K_Y80c2Ff8DRK
33
33
  arrayview/_routes_websocket.py,sha256=V5k_LhVw2pafItcuK5Q5kYYR4kmGfCRcaz7NRFG43S0,29043
34
34
  arrayview/_segmentation.py,sha256=aENWHmefrKDScXhcZL1X3KgYXE784dpMkmFFo83qnrw,7377
35
35
  arrayview/_server.py,sha256=NV6tRHo2OVR-PuSutGDAkYYSafD2exZfggUoUcXzti0,11797
36
- arrayview/_session.py,sha256=GFcReSWye7SMryaEIuqy5QjQCmjngsk4WUvvWG0tw6A,22330
36
+ arrayview/_session.py,sha256=eNE4GQBhvKKCAB_DkfIpjVDMBRo4CD9X3pJbaZmNVH8,22641
37
37
  arrayview/_session_spec.py,sha256=mKDfMRsPYQdEmN9hLXmDU_9N7etiDyBXr8mz-fB8QBo,6825
38
38
  arrayview/_shell.html,sha256=vNa2Kiwx_eoB5H329jolYafLhwJDCH9DsJafBqvVMWU,13194
39
39
  arrayview/_synthetic_mri.py,sha256=CNL-2WC835i6WCqkvmWBCO6WQ5w7HE5trNb8OyEfe64,7365
40
40
  arrayview/_torch.py,sha256=13sqIMCUu_-b9SQ9KfDCExDz2UMStL87EDVjnbJxAEo,7536
41
41
  arrayview/_vectorfield.py,sha256=eDqMCMEGjO8Jdt-6Auh01cz3WD9dbF7xYUWD9xHAf1E,8115
42
- arrayview/_viewer.html,sha256=26xtUFu0g6ZNfBy_bcHLYpQp79f6WtLHgWKOIqEJB1I,1884918
42
+ arrayview/_viewer.html,sha256=jwqjyk4Vl5aUFAVdGmM-CRETldBcqpVIZmUSO9LZ0Jc,1887238
43
43
  arrayview/_vscode.py,sha256=EKnGN2zrjlybc4FQOmYA8BvYdtkcJbLHDEdgtvO7ul4,837
44
44
  arrayview/_vscode_browser.py,sha256=kTdux_l-xax5qYEX2zU5xGscceBrnlZUaX1i28bZ0jI,18950
45
- arrayview/_vscode_extension.py,sha256=BNIaMjbVOANwO4TmT5Lxk3kwAxEujKg2WcZQtYYLva8,28552
45
+ arrayview/_vscode_extension.py,sha256=n4dcV5x2gxCx3o0ghd2tmtJYLVwrhYDp9GEQKBQFS_M,30137
46
46
  arrayview/_vscode_signal.py,sha256=8pGVjuzUzIu9bNSZkczF8guzblS1oHyefFplAebrjBU,45559
47
- arrayview/arrayview-opener.vsix,sha256=zvqqtzbl3kW9W7GGkACA5XvtIJkpj4NqdYauMnN4r7Y,34699
47
+ arrayview/arrayview-opener.vsix,sha256=zYr0bpA2WN_oL8sNq0vCF4SqsRZlNiDncl-K7A8o3PM,38458
48
48
  arrayview/gsap.min.js,sha256=VGihm4idI0E1uqrWezWsiScB9F_jMosn7qs6pPSkeac,72304
49
49
  arrayview/todo.md,sha256=4rzoaIY4nJJACDHPJKeqP8guasCMxeRQ1cyNLxUYink,999
50
- arrayview-0.35.2.dist-info/METADATA,sha256=7vfM0N_tScssKX51C6CKVl31xg_IcrmC-okuesY8_-k,2345
51
- arrayview-0.35.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
52
- arrayview-0.35.2.dist-info/entry_points.txt,sha256=FQSz3M11B-zaVLDyXofKUDvMhHASz7OjmoLgI4nn7ko,105
53
- arrayview-0.35.2.dist-info/licenses/LICENSE,sha256=34OivjM9faB2QaPjZpMRTIiSx31V3U9cEyQ6F2k-bH0,1062
54
- arrayview-0.35.2.dist-info/RECORD,,
50
+ arrayview-0.36.0.dist-info/METADATA,sha256=RFS3W_onLeEjsc_EGYKynB7-kqPYdGnEgx3LziJ-bnU,2345
51
+ arrayview-0.36.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
52
+ arrayview-0.36.0.dist-info/entry_points.txt,sha256=FQSz3M11B-zaVLDyXofKUDvMhHASz7OjmoLgI4nn7ko,105
53
+ arrayview-0.36.0.dist-info/licenses/LICENSE,sha256=34OivjM9faB2QaPjZpMRTIiSx31V3U9cEyQ6F2k-bH0,1062
54
+ arrayview-0.36.0.dist-info/RECORD,,