voltsdk 2.2.2__tar.gz → 2.2.4__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.
Files changed (42) hide show
  1. {voltsdk-2.2.2 → voltsdk-2.2.4}/PKG-INFO +69 -8
  2. {voltsdk-2.2.2 → voltsdk-2.2.4}/pyproject.toml +1 -1
  3. voltsdk-2.2.4/tests/test_registry.py +67 -0
  4. voltsdk-2.2.4/tests/test_spatial.py +76 -0
  5. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/README.md +68 -7
  6. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/__init__.py +13 -1
  7. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/errors.py +1 -1
  8. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/plugin.py +97 -1
  9. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/registry.py +2 -22
  10. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/analyses.py +17 -0
  11. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/exposures.py +22 -0
  12. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/frames.py +17 -0
  13. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/trajectories.py +18 -0
  14. voltsdk-2.2.4/voltsdk/spatial.py +1178 -0
  15. voltsdk-2.2.4/voltsdk/viewer.py +520 -0
  16. voltsdk-2.2.4/voltsdk/viewer_server.py +46 -0
  17. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/PKG-INFO +69 -8
  18. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/SOURCES.txt +5 -0
  19. {voltsdk-2.2.2 → voltsdk-2.2.4}/setup.cfg +0 -0
  20. {voltsdk-2.2.2 → voltsdk-2.2.4}/setup.py +0 -0
  21. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/client.py +0 -0
  22. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/exceptions.py +0 -0
  23. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/http.py +0 -0
  24. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/integrations/__init__.py +0 -0
  25. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/integrations/glb.py +0 -0
  26. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/integrations/ovito.py +0 -0
  27. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/integrations/plotting.py +0 -0
  28. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/io/__init__.py +0 -0
  29. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/io/downloads.py +0 -0
  30. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/io/msgpack.py +0 -0
  31. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/native/__init__.py +0 -0
  32. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/__init__.py +0 -0
  33. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/hub.py +0 -0
  34. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/__init__.py +0 -0
  35. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/base.py +0 -0
  36. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/listings.py +0 -0
  37. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/simulation_cells.py +0 -0
  38. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/teams.py +0 -0
  39. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/dependency_links.txt +0 -0
  40. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/not-zip-safe +0 -0
  41. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/requires.txt +0 -0
  42. {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voltsdk
3
- Version: 2.2.2
3
+ Version: 2.2.4
4
4
  Summary: Python SDK for the Volt scientific computing platform
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -65,6 +65,68 @@ client = VoltClient(
65
65
  )
66
66
  ```
67
67
 
68
+ ## Open In Volt
69
+
70
+ Use the Volt web canvas as the viewer for local GLBs, GLB sequences, or
71
+ existing Volt resources:
72
+
73
+ ```python
74
+ from voltsdk import open_in_volt
75
+
76
+ open_in_volt('out/frame.glb')
77
+ open_in_volt({
78
+ 0: 'out/frame-0000.glb',
79
+ 5000: 'out/frame-5000.glb',
80
+ 10000: 'out/frame-10000.glb',
81
+ })
82
+ ```
83
+
84
+ Resource helpers open the exact canvas route:
85
+
86
+ ```python
87
+ trajectory.open_in_volt(timestep=5000)
88
+ frame.open_in_volt()
89
+ analysis.open_in_volt(timestep=5000)
90
+ exposure.open_in_volt(5000)
91
+ run.open_in_volt('dislocations')
92
+ ```
93
+
94
+ For plugin runs, `open_in_volt(...)` auto-assembles supported `.json` and
95
+ `.msgpack` exporter payloads into a sibling `.glb` when needed.
96
+
97
+ For local files, VoltSDK serves them directly to `/canvas/glb` without creating
98
+ fake analyses. In a Volt notebook it uses the Jupyter proxy; on a local machine
99
+ it starts a tiny background file server. The browser still needs an authenticated
100
+ Volt session. By default the viewer opens against `https://app.voltcloud.dev`;
101
+ override it with `volt_url=...` or `VOLT_APP_URL` if needed.
102
+
103
+ ## SpatialAssembler
104
+
105
+ Volt exporter payloads can also be rendered locally from Python:
106
+
107
+ ```python
108
+ from voltsdk import SpatialAssembler, open_in_volt
109
+
110
+ assembler = SpatialAssembler()
111
+ glb_path = assembler.dislocations_glb(
112
+ 'output/ptm-dxa_dislocations.json',
113
+ output_path='output/ptm-dxa_dislocations.glb',
114
+ )
115
+
116
+ open_in_volt(glb_path)
117
+ ```
118
+
119
+ The Python API accepts either the full artifact file (`.json` or `.msgpack`) or
120
+ the raw nested payload from `export.AtomisticExporter`, `export.MeshExporter`,
121
+ or `export.DislocationExporter`.
122
+
123
+ You can also let a `PluginRun` do the conversion directly:
124
+
125
+ ```python
126
+ viewer_url = dxa_run.open_in_volt('dislocations', open_browser=False)
127
+ mesh_glb = dxa_run.glb('defect_mesh')
128
+ ```
129
+
68
130
  ## Plugin hub
69
131
 
70
132
  VoltSDK ships a Hugging Face-style plugin hub. Plugins live in a static
@@ -80,10 +142,10 @@ ptm = hub.get("voltlabs@polyhedral-template-matching")
80
142
  result = ptm.run(
81
143
  "frame.dump",
82
144
  output_base="out/frame",
83
- crystalStructure="FCC",
145
+ crystal_structure="FCC",
84
146
  rmsd=0.1,
85
147
  )
86
- print(result.artifact("annotatedDump"))
148
+ print(result.path("annotated.dump"))
87
149
  ```
88
150
 
89
151
  The same hub is exposed on an authenticated client via `client.plugins`.
@@ -129,13 +191,12 @@ The hub expects a static index plus per-platform bundles:
129
191
  "publisher": "voltlabs",
130
192
  "latest": "1.0.0",
131
193
  "versions": {
132
- "1.0.0": {
133
- "linux-x86_64": {
134
- "url": "opendxa/1.0.0/linux-x86_64.tar.zst",
135
- "sha256": "..."
194
+ "1.0.0": {
195
+ "linux-x86_64": {
196
+ "url": "opendxa/1.0.0/linux-x86_64.tar.zst"
197
+ }
136
198
  }
137
199
  }
138
- }
139
200
  }
140
201
  }
141
202
  }
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voltsdk"
7
- version = "2.2.2"
7
+ version = "2.2.4"
8
8
  description = "Python SDK for the Volt scientific computing platform"
9
9
  readme = "voltsdk/README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import tarfile
5
+ import tempfile
6
+ import unittest
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
11
+
12
+ from voltsdk.plugins.registry import PluginRegistry
13
+
14
+
15
+ def _write_bundle(path: Path) -> None:
16
+ with tempfile.TemporaryDirectory() as bundle_tmp:
17
+ root = Path(bundle_tmp)
18
+ bin_dir = root / 'bin'
19
+ bin_dir.mkdir(parents=True, exist_ok=True)
20
+ executable = bin_dir / 'demo-plugin'
21
+ executable.write_text('#!/bin/sh\nexit 0\n', encoding='utf-8')
22
+ with tarfile.open(path, 'w:gz') as archive:
23
+ archive.add(bin_dir, arcname='bin')
24
+
25
+
26
+ class PluginRegistryTests(unittest.TestCase):
27
+ def test_install_ignores_sha256_metadata(self) -> None:
28
+ with tempfile.TemporaryDirectory() as tmp:
29
+ root = Path(tmp)
30
+ bundle_path = root / 'demo-plugin-1.0.0-linux-x86_64.tgz'
31
+ _write_bundle(bundle_path)
32
+
33
+ index = {
34
+ 'plugins': {
35
+ 'voltlabs': {
36
+ 'demo-plugin': {
37
+ 'publisher': 'voltlabs',
38
+ 'latest': '1.0.0',
39
+ 'versions': {
40
+ '1.0.0': {
41
+ 'linux-x86_64': {
42
+ 'url': bundle_path.name,
43
+ 'sha256': 'definitely-wrong',
44
+ },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ }
51
+ index_path = root / 'index.json'
52
+ index_path.write_text(json.dumps(index), encoding='utf-8')
53
+
54
+ registry = PluginRegistry(
55
+ url=index_path.as_uri(),
56
+ cache_dir=root / 'cache',
57
+ platform_tag='linux-x86_64',
58
+ )
59
+
60
+ installed = registry.install('voltlabs@demo-plugin')
61
+
62
+ self.assertTrue(installed.is_dir())
63
+ self.assertTrue((installed / 'bin' / 'demo-plugin').is_file())
64
+
65
+
66
+ if __name__ == '__main__':
67
+ unittest.main()
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+ import sys
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
+
11
+ from voltsdk.plugins.plugin import PluginRun
12
+ from voltsdk.spatial import SpatialAssembler
13
+
14
+
15
+ def _sample_dislocation_payload() -> dict:
16
+ return {
17
+ 'export': {
18
+ 'DislocationExporter': {
19
+ 'segments': [
20
+ {
21
+ 'points': [
22
+ [0.0, 0.0, 0.0],
23
+ [1.0, 0.0, 0.0],
24
+ [1.0, 1.0, 0.0],
25
+ ],
26
+ 'burgers': {
27
+ 'vector': [0.5, 0.5, 0.5],
28
+ },
29
+ },
30
+ ],
31
+ },
32
+ },
33
+ }
34
+
35
+
36
+ class SpatialAssemblerTests(unittest.TestCase):
37
+ def test_glb_infers_dislocation_export_from_json(self) -> None:
38
+ with tempfile.TemporaryDirectory() as tmp:
39
+ root = Path(tmp)
40
+ payload_path = root / 'dislocations.json'
41
+ output_path = root / 'dislocations.glb'
42
+ payload_path.write_text(json.dumps(_sample_dislocation_payload()), encoding='utf-8')
43
+
44
+ result = SpatialAssembler().glb(payload_path, output_path=output_path)
45
+
46
+ self.assertEqual(result, output_path)
47
+ self.assertTrue(output_path.is_file())
48
+ self.assertEqual(output_path.read_bytes()[:4], b'glTF')
49
+
50
+ def test_plugin_run_glb_writes_sibling_glb(self) -> None:
51
+ with tempfile.TemporaryDirectory() as tmp:
52
+ root = Path(tmp)
53
+ payload_path = root / 'output_dislocations.json'
54
+ payload_path.write_text(json.dumps(_sample_dislocation_payload()), encoding='utf-8')
55
+
56
+ run = PluginRun(
57
+ command=[],
58
+ returncode=0,
59
+ stdout='',
60
+ stderr='',
61
+ output_base=root / 'output',
62
+ artifacts={
63
+ 'dislocations': payload_path,
64
+ 'dislocations.json': payload_path,
65
+ },
66
+ )
67
+
68
+ result = run.glb('dislocations')
69
+
70
+ self.assertEqual(result, root / 'output_dislocations.glb')
71
+ self.assertTrue(result.is_file())
72
+ self.assertEqual(result.read_bytes()[:4], b'glTF')
73
+
74
+
75
+ if __name__ == '__main__':
76
+ unittest.main()
@@ -41,6 +41,68 @@ client = VoltClient(
41
41
  )
42
42
  ```
43
43
 
44
+ ## Open In Volt
45
+
46
+ Use the Volt web canvas as the viewer for local GLBs, GLB sequences, or
47
+ existing Volt resources:
48
+
49
+ ```python
50
+ from voltsdk import open_in_volt
51
+
52
+ open_in_volt('out/frame.glb')
53
+ open_in_volt({
54
+ 0: 'out/frame-0000.glb',
55
+ 5000: 'out/frame-5000.glb',
56
+ 10000: 'out/frame-10000.glb',
57
+ })
58
+ ```
59
+
60
+ Resource helpers open the exact canvas route:
61
+
62
+ ```python
63
+ trajectory.open_in_volt(timestep=5000)
64
+ frame.open_in_volt()
65
+ analysis.open_in_volt(timestep=5000)
66
+ exposure.open_in_volt(5000)
67
+ run.open_in_volt('dislocations')
68
+ ```
69
+
70
+ For plugin runs, `open_in_volt(...)` auto-assembles supported `.json` and
71
+ `.msgpack` exporter payloads into a sibling `.glb` when needed.
72
+
73
+ For local files, VoltSDK serves them directly to `/canvas/glb` without creating
74
+ fake analyses. In a Volt notebook it uses the Jupyter proxy; on a local machine
75
+ it starts a tiny background file server. The browser still needs an authenticated
76
+ Volt session. By default the viewer opens against `https://app.voltcloud.dev`;
77
+ override it with `volt_url=...` or `VOLT_APP_URL` if needed.
78
+
79
+ ## SpatialAssembler
80
+
81
+ Volt exporter payloads can also be rendered locally from Python:
82
+
83
+ ```python
84
+ from voltsdk import SpatialAssembler, open_in_volt
85
+
86
+ assembler = SpatialAssembler()
87
+ glb_path = assembler.dislocations_glb(
88
+ 'output/ptm-dxa_dislocations.json',
89
+ output_path='output/ptm-dxa_dislocations.glb',
90
+ )
91
+
92
+ open_in_volt(glb_path)
93
+ ```
94
+
95
+ The Python API accepts either the full artifact file (`.json` or `.msgpack`) or
96
+ the raw nested payload from `export.AtomisticExporter`, `export.MeshExporter`,
97
+ or `export.DislocationExporter`.
98
+
99
+ You can also let a `PluginRun` do the conversion directly:
100
+
101
+ ```python
102
+ viewer_url = dxa_run.open_in_volt('dislocations', open_browser=False)
103
+ mesh_glb = dxa_run.glb('defect_mesh')
104
+ ```
105
+
44
106
  ## Plugin hub
45
107
 
46
108
  VoltSDK ships a Hugging Face-style plugin hub. Plugins live in a static
@@ -56,10 +118,10 @@ ptm = hub.get("voltlabs@polyhedral-template-matching")
56
118
  result = ptm.run(
57
119
  "frame.dump",
58
120
  output_base="out/frame",
59
- crystalStructure="FCC",
121
+ crystal_structure="FCC",
60
122
  rmsd=0.1,
61
123
  )
62
- print(result.artifact("annotatedDump"))
124
+ print(result.path("annotated.dump"))
63
125
  ```
64
126
 
65
127
  The same hub is exposed on an authenticated client via `client.plugins`.
@@ -105,13 +167,12 @@ The hub expects a static index plus per-platform bundles:
105
167
  "publisher": "voltlabs",
106
168
  "latest": "1.0.0",
107
169
  "versions": {
108
- "1.0.0": {
109
- "linux-x86_64": {
110
- "url": "opendxa/1.0.0/linux-x86_64.tar.zst",
111
- "sha256": "..."
170
+ "1.0.0": {
171
+ "linux-x86_64": {
172
+ "url": "opendxa/1.0.0/linux-x86_64.tar.zst"
173
+ }
112
174
  }
113
175
  }
114
- }
115
176
  }
116
177
  }
117
178
  }
@@ -35,7 +35,7 @@ from .plugins import (
35
35
  )
36
36
  from .native import root as native_root
37
37
 
38
- __version__ = "2.2.2"
38
+ __version__ = "2.2.4"
39
39
 
40
40
  __all__ = [
41
41
  "VoltClient",
@@ -47,6 +47,8 @@ __all__ = [
47
47
  "VoltPermissionError",
48
48
  "VoltTimeoutError",
49
49
  "msgpack_as_df",
50
+ "open_in_volt",
51
+ "SpatialAssembler",
50
52
  "view_glb",
51
53
  "Plugin",
52
54
  "PluginError",
@@ -69,6 +71,16 @@ def __getattr__(name: str):
69
71
 
70
72
  globals()[name] = msgpack_as_df
71
73
  return msgpack_as_df
74
+ if name == "open_in_volt":
75
+ from .viewer import open_in_volt
76
+
77
+ globals()[name] = open_in_volt
78
+ return open_in_volt
79
+ if name == "SpatialAssembler":
80
+ from .spatial import SpatialAssembler
81
+
82
+ globals()[name] = SpatialAssembler
83
+ return SpatialAssembler
72
84
  if name == "view_glb":
73
85
  from .integrations.glb import view_glb
74
86
 
@@ -10,4 +10,4 @@ class PluginNotFoundError(PluginError):
10
10
 
11
11
 
12
12
  class PluginVerificationError(PluginError):
13
- """Raised when a downloaded bundle fails sha256 verification."""
13
+ """Reserved for plugin bundle validation failures."""
@@ -34,6 +34,57 @@ class PluginRun:
34
34
  return artifact
35
35
  return None
36
36
 
37
+ def path(self, name: str) -> Path:
38
+ artifact = self.artifact(name)
39
+ if artifact is not None:
40
+ return artifact
41
+ available = ', '.join(sorted(self.artifacts)) or '<none>'
42
+ raise PluginError(f'Artifact {name!r} not found. Available artifacts: {available}.')
43
+
44
+ def df(self, name: str, key: str | None = None):
45
+ path = self.path(name)
46
+ suffix = path.suffix.lower()
47
+ if suffix == '.msgpack':
48
+ from ..io.msgpack import msgpack_as_df
49
+
50
+ return msgpack_as_df(str(path), key)
51
+ if suffix == '.json':
52
+ return _json_df(path, key)
53
+ raise PluginError(f'Artifact {path.name!r} is not a supported dataframe source.')
54
+
55
+ def glb(
56
+ self,
57
+ name: str,
58
+ *,
59
+ output_path: str | os.PathLike[str] | None = None,
60
+ exporter: str | None = None,
61
+ **options: Any,
62
+ ) -> Path:
63
+ from ..spatial import SpatialAssembler
64
+
65
+ path = self.path(name)
66
+ if _is_glb_artifact(path):
67
+ return path
68
+
69
+ target = Path(output_path).expanduser().resolve() if output_path else _default_glb_output_path(path)
70
+ result = SpatialAssembler().glb(path, output_path=target, exporter=exporter, **options)
71
+ if not isinstance(result, Path):
72
+ raise PluginError('SpatialAssembler did not return a file path.')
73
+ return result
74
+
75
+ def open_in_volt(
76
+ self,
77
+ name: str,
78
+ *,
79
+ volt_url: str | None = None,
80
+ open_browser: bool = True,
81
+ ) -> str:
82
+ from ..viewer import open_in_volt
83
+
84
+ path = self.path(name)
85
+ source = path if _is_glb_artifact(path) else self.glb(name)
86
+ return open_in_volt(source, volt_url=volt_url, open_browser=open_browser)
87
+
37
88
 
38
89
  class Plugin:
39
90
  """A single plugin instance backed by a downloaded bundle.
@@ -75,7 +126,7 @@ class Plugin:
75
126
  ) -> PluginRun:
76
127
  config = _prepare_config({**self.options, **options})
77
128
  input_path = Path(input_file).expanduser().resolve()
78
- output = Path(output_base).expanduser() if output_base else input_path.with_suffix("")
129
+ output = Path(output_base).expanduser().resolve() if output_base else input_path.with_suffix("")
79
130
  output.parent.mkdir(parents=True, exist_ok=True)
80
131
 
81
132
  command, completed = self._run_subprocess(
@@ -271,6 +322,51 @@ def _canonical_artifact_name(prefix: str, filename: str) -> str:
271
322
  return filename
272
323
 
273
324
 
325
+ def _is_glb_artifact(path: Path) -> bool:
326
+ suffix = path.suffix.lower()
327
+ return suffix in {'.glb', '.gltf'} or path.name.lower().endswith('.glb.zst')
328
+
329
+
330
+ def _default_glb_output_path(path: Path) -> Path:
331
+ suffix = path.suffix.lower()
332
+ if suffix:
333
+ return path.with_suffix('.glb')
334
+ return path.with_name(f'{path.name}.glb')
335
+
336
+
337
+ def _json_df(path: Path, key: str | None):
338
+ import pandas as pd
339
+
340
+ from ..io.msgpack import get_nested_value
341
+
342
+ with path.open('r', encoding='utf-8') as fh:
343
+ data = get_nested_value(json.load(fh), key)
344
+ return _data_as_df(data, pd)
345
+
346
+
347
+ def _data_as_df(data: Any, pd):
348
+ if data is None:
349
+ return pd.DataFrame()
350
+ if isinstance(data, list):
351
+ return pd.DataFrame(data)
352
+ if _is_columnar_dict(data):
353
+ return pd.DataFrame(data)
354
+ if isinstance(data, dict):
355
+ return pd.DataFrame([data])
356
+ return pd.DataFrame([{'value': data}])
357
+
358
+
359
+ def _is_columnar_dict(value: Any) -> bool:
360
+ if not isinstance(value, dict) or not value:
361
+ return False
362
+ lengths: list[int] = []
363
+ for item in value.values():
364
+ if not isinstance(item, list):
365
+ return False
366
+ lengths.append(len(item))
367
+ return len(set(lengths)) == 1
368
+
369
+
274
370
  def _env(root: Path) -> dict[str, str]:
275
371
  env = os.environ.copy()
276
372
  plugin_bin_dir = root / "bin"
@@ -11,8 +11,7 @@ Index format (``GET <registry>/index.json``)::
11
11
  "versions": {
12
12
  "1.0.0": {
13
13
  "linux-x86_64": {
14
- "url": "opendxa/1.0.0/linux-x86_64.tar.zst",
15
- "sha256": "..."
14
+ "url": "opendxa/1.0.0/linux-x86_64.tar.zst"
16
15
  }
17
16
  }
18
17
  }
@@ -32,7 +31,6 @@ Layout inside a bundle (after extraction)::
32
31
 
33
32
  from __future__ import annotations
34
33
 
35
- import hashlib
36
34
  import os
37
35
  import platform
38
36
  import shutil
@@ -43,7 +41,7 @@ from dataclasses import dataclass
43
41
  from pathlib import Path
44
42
  from typing import Any
45
43
 
46
- from .errors import PluginNotFoundError, PluginVerificationError
44
+ from .errors import PluginNotFoundError
47
45
 
48
46
  DEFAULT_REGISTRY_URL = "https://server.voltcloud.dev/plugin-registry"
49
47
  INDEX_PATH = "index.json"
@@ -56,7 +54,6 @@ class BundleRef:
56
54
  version: str
57
55
  platform: str
58
56
  url: str
59
- sha256: str
60
57
 
61
58
 
62
59
  class PluginRegistry:
@@ -121,7 +118,6 @@ class PluginRegistry:
121
118
  version=version,
122
119
  platform=self.platform_tag,
123
120
  url=str(platform_entry["url"]),
124
- sha256=str(platform_entry.get("sha256", "")),
125
121
  )
126
122
 
127
123
  # ------------------------------------------------------------------
@@ -135,14 +131,6 @@ class PluginRegistry:
135
131
  return target
136
132
 
137
133
  archive = self._fetch(ref.url, f"{ref.key}-{ref.version}-{ref.platform}")
138
- if ref.sha256:
139
- actual = _sha256(archive)
140
- if actual != ref.sha256:
141
- archive.unlink(missing_ok=True)
142
- raise PluginVerificationError(
143
- f"sha256 mismatch for {ref.key}@{ref.version}: expected {ref.sha256}, got {actual}."
144
- )
145
-
146
134
  if target.exists():
147
135
  shutil.rmtree(target)
148
136
  target.mkdir(parents=True, exist_ok=True)
@@ -239,14 +227,6 @@ def _is_absolute(url: str) -> bool:
239
227
  return urllib.parse.urlparse(url).scheme in {"http", "https", "file"}
240
228
 
241
229
 
242
- def _sha256(path: Path) -> str:
243
- hasher = hashlib.sha256()
244
- with path.open("rb") as fh:
245
- for chunk in iter(lambda: fh.read(1 << 20), b""):
246
- hasher.update(chunk)
247
- return hasher.hexdigest()
248
-
249
-
250
230
  def _extract(archive: Path, target: Path) -> None:
251
231
  if archive.suffixes[-2:] == [".tar", ".zst"]:
252
232
  import zstandard
@@ -101,6 +101,23 @@ class Analysis(BaseResource):
101
101
  return self._client.unzip_recursive(zip_path)
102
102
  return zip_path
103
103
 
104
+ def open_in_volt(
105
+ self,
106
+ *,
107
+ timestep: int | None = None,
108
+ volt_url: str | None = None,
109
+ open_browser: bool = True,
110
+ ) -> str:
111
+ from voltsdk.viewer import open_canvas_view
112
+
113
+ return open_canvas_view(
114
+ trajectory_id=self.trajectory_id,
115
+ analysis_id=self.id,
116
+ timestep=timestep,
117
+ volt_url=volt_url,
118
+ open_browser=open_browser,
119
+ )
120
+
104
121
 
105
122
  class AnalysisCollection(BaseCollection['Analysis']):
106
123
  """Paginated collection of analyses.
@@ -101,6 +101,28 @@ class Exposure(BaseResource):
101
101
  dest=dest,
102
102
  )
103
103
 
104
+ def open_in_volt(
105
+ self,
106
+ timestep: int,
107
+ *,
108
+ volt_url: str | None = None,
109
+ open_browser: bool = True,
110
+ ) -> str:
111
+ from voltsdk.viewer import open_canvas_view
112
+
113
+ trajectory_id = self._get('trajectory', '')
114
+ if not trajectory_id:
115
+ raise ValueError('Exposure does not include a trajectory ID.')
116
+
117
+ return open_canvas_view(
118
+ trajectory_id=trajectory_id,
119
+ analysis_id=self.analysis_id,
120
+ exposure_id=self.id,
121
+ timestep=timestep,
122
+ volt_url=volt_url,
123
+ open_browser=open_browser,
124
+ )
125
+
104
126
 
105
127
  class ExposureCollection(BaseCollection['Exposure']):
106
128
  """Paginated collection of exposures for an analysis."""
@@ -105,6 +105,23 @@ class Frame:
105
105
  dest=dest,
106
106
  )
107
107
 
108
+ def open_in_volt(
109
+ self,
110
+ analysis_id: str = 'default',
111
+ *,
112
+ volt_url: str | None = None,
113
+ open_browser: bool = True,
114
+ ) -> str:
115
+ from voltsdk.viewer import open_canvas_view
116
+
117
+ return open_canvas_view(
118
+ trajectory_id=self._trajectory_id,
119
+ analysis_id=analysis_id,
120
+ timestep=self.timestep,
121
+ volt_url=volt_url,
122
+ open_browser=open_browser,
123
+ )
124
+
108
125
  # ------------------------------------------------------------------
109
126
  # OVITO integration
110
127
  # ------------------------------------------------------------------
@@ -95,6 +95,24 @@ class Trajectory(BaseResource):
95
95
  dest=dest,
96
96
  )
97
97
 
98
+ def open_in_volt(
99
+ self,
100
+ *,
101
+ analysis_id: str | None = None,
102
+ timestep: int | None = None,
103
+ volt_url: str | None = None,
104
+ open_browser: bool = True,
105
+ ) -> str:
106
+ from voltsdk.viewer import open_canvas_view
107
+
108
+ return open_canvas_view(
109
+ trajectory_id=self.id,
110
+ analysis_id=analysis_id,
111
+ timestep=timestep,
112
+ volt_url=volt_url,
113
+ open_browser=open_browser,
114
+ )
115
+
98
116
  # ------------------------------------------------------------------
99
117
  # Plotting
100
118
  # ------------------------------------------------------------------