voltsdk 0.1.0__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.
voltsdk-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: voltsdk
3
+ Version: 0.1.0
4
+ Summary: Volt API SDK for scripting and notebooks
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: msgpack>=1.0.0
8
+ Requires-Dist: pandas>=2.0.0
9
+ Requires-Dist: requests>=2.31.0
10
+ Provides-Extra: visualization
11
+ Requires-Dist: vtk>=9.2.0; extra == "visualization"
12
+ Provides-Extra: notebook
13
+ Requires-Dist: vtk>=9.2.0; extra == "notebook"
14
+ Requires-Dist: k3d>=2.16.0; extra == "notebook"
15
+
16
+ # voltsdk
17
+
18
+ Python SDK for interacting with the Volt API.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install voltsdk
24
+ ```
25
+
26
+ Install visualization support with:
27
+
28
+ ```bash
29
+ pip install "voltsdk[visualization]"
30
+ ```
31
+
32
+ Install notebook support with:
33
+
34
+ ```bash
35
+ pip install "voltsdk[notebook]"
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ from voltsdk import VoltClient
42
+
43
+ client = VoltClient(
44
+ secret_key="your-secret-key",
45
+ base_url="https://api.example.com"
46
+ )
47
+ ```
48
+
@@ -0,0 +1,33 @@
1
+ # voltsdk
2
+
3
+ Python SDK for interacting with the Volt API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install voltsdk
9
+ ```
10
+
11
+ Install visualization support with:
12
+
13
+ ```bash
14
+ pip install "voltsdk[visualization]"
15
+ ```
16
+
17
+ Install notebook support with:
18
+
19
+ ```bash
20
+ pip install "voltsdk[notebook]"
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from voltsdk import VoltClient
27
+
28
+ client = VoltClient(
29
+ secret_key="your-secret-key",
30
+ base_url="https://api.example.com"
31
+ )
32
+ ```
33
+
@@ -0,0 +1,4 @@
1
+ from .client import VoltClient
2
+ from .utils import msgpack_as_df, view_glb
3
+
4
+ __all__ = ["VoltClient", "view_glb", "msgpack_as_df"]
@@ -0,0 +1,269 @@
1
+ from typing import Any, Dict, Optional
2
+ import os
3
+ import requests
4
+ import zipfile
5
+
6
+
7
+ class VoltClient:
8
+ def __init__(
9
+ self,
10
+ secret_key: str,
11
+ base_url: Optional[str] = None,
12
+ timeout: int = 30
13
+ ) -> None:
14
+ if not secret_key:
15
+ raise ValueError('secret_key is required')
16
+
17
+ if not base_url:
18
+ raise ValueError('base_url is required')
19
+
20
+ self.secret_key = secret_key
21
+ self.base_url = base_url
22
+ self.timeout = timeout
23
+
24
+ self._secret_key_info: Optional[Dict[str, Any]] = None
25
+ self._team_id: Optional[str] = None
26
+
27
+ self.session = requests.Session()
28
+ self.session.headers.update({
29
+ 'Authorization': f'Bearer {secret_key}',
30
+ 'Accept': 'application/json'
31
+ })
32
+
33
+ self._setup_client()
34
+
35
+ def _request(
36
+ self,
37
+ method: str,
38
+ path: str,
39
+ *,
40
+ params: Optional[Dict[str, Any]] = None
41
+ ) -> Any:
42
+ response = self._send(
43
+ method,
44
+ path,
45
+ params=params
46
+ )
47
+
48
+ payload = response.json()
49
+ if payload.get('status') != 'success':
50
+ raise RuntimeError(payload.get('message') or 'Volt API request faield')
51
+
52
+ return payload.get('data')
53
+
54
+ def _send(
55
+ self,
56
+ method: str,
57
+ path: str,
58
+ *,
59
+ params: Optional[Dict[str, Any]] = None,
60
+ stream: bool = False
61
+ ) -> requests.Response:
62
+ url = f"{self.base_url.rstrip('/')}/{path.lstrip('/')}"
63
+ response = self.session.request(
64
+ method=method,
65
+ url=url,
66
+ params=params,
67
+ timeout=self.timeout,
68
+ stream=stream
69
+ )
70
+
71
+ if response.status_code >= 400:
72
+ try:
73
+ payload = response.json()
74
+ message = payload.get('message') or response.text
75
+ except Exception:
76
+ message = response.text or response.reason
77
+
78
+ raise requests.HTTPError(
79
+ f'{response.status_code} {response.reason}: {message} ({response.url})',
80
+ response=response
81
+ )
82
+
83
+ return response
84
+
85
+ def _setup_client(self) -> Dict[str, Any]:
86
+ data = self._request('GET', '/team/secret-keys/me')
87
+
88
+ self._secret_key_info = data
89
+ self._team_id = data.get('team')
90
+ return data
91
+
92
+ def _resolve_download_filename(
93
+ self,
94
+ response: requests.Response,
95
+ fallback_name: str
96
+ ) -> str:
97
+ content_disposition = response.headers.get('Content-Disposition', '')
98
+ if 'filename=' not in content_disposition:
99
+ return fallback_name
100
+
101
+ filename = content_disposition.split('filename=')[-1].strip().strip('"').strip("'")
102
+ if not filename:
103
+ return fallback_name
104
+
105
+ return os.path.basename(filename)
106
+
107
+ def _download_stream(
108
+ self,
109
+ path: str,
110
+ fallback_name: str
111
+ ) -> str:
112
+ with self._send('GET', path, stream=True) as response:
113
+ downloads_dir = './downloads'
114
+ os.makedirs(downloads_dir, exist_ok=True)
115
+
116
+ filename = self._resolve_download_filename(response, fallback_name)
117
+ file_path = os.path.join(downloads_dir, filename)
118
+ total_bytes = int(response.headers.get('Content-Length', 0) or 0)
119
+ downloaded_bytes = 0
120
+
121
+ with open(file_path, 'wb') as file:
122
+ for chunk in response.iter_content(chunk_size=8192):
123
+ if not chunk:
124
+ continue
125
+
126
+ file.write(chunk)
127
+ downloaded_bytes += len(chunk)
128
+
129
+ if total_bytes > 0:
130
+ percent = (downloaded_bytes * 100) / total_bytes
131
+ print(
132
+ f'\rDownloading {filename}: {percent:.2f}% ({downloaded_bytes}/{total_bytes} bytes)',
133
+ end='',
134
+ flush=True
135
+ )
136
+ else:
137
+ print(
138
+ f'\rDownloading {filename}: {downloaded_bytes} bytes',
139
+ end='',
140
+ flush=True
141
+ )
142
+
143
+ print()
144
+
145
+ return file_path
146
+
147
+ def _extract_zip_file(self, zip_path: str) -> str:
148
+ extract_dir = zip_path[:-4]
149
+ os.makedirs(extract_dir, exist_ok=True)
150
+
151
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
152
+ zip_ref.extractall(extract_dir)
153
+
154
+ os.remove(zip_path)
155
+ return extract_dir
156
+
157
+ def _unzip_recursive(self, zip_path: str) -> str:
158
+ if not zip_path.lower().endswith('.zip'):
159
+ return zip_path
160
+
161
+ root_dir = self._extract_zip_file(zip_path)
162
+ while True:
163
+ zip_files = []
164
+ for current_root, _, files in os.walk(root_dir):
165
+ for file_name in files:
166
+ if file_name.lower().endswith('.zip'):
167
+ zip_files.append(os.path.join(current_root, file_name))
168
+
169
+ if not zip_files:
170
+ break
171
+
172
+ total_zip_files = len(zip_files)
173
+ for index, current_zip_path in enumerate(zip_files, start=1):
174
+ print(
175
+ f'Extracting nested zip {index}/{total_zip_files}: {os.path.basename(current_zip_path)}',
176
+ flush=True
177
+ )
178
+ self._extract_zip_file(current_zip_path)
179
+
180
+ return root_dir
181
+
182
+ def list_analyses(
183
+ self,
184
+ trajectory_id: str,
185
+ page: int = 1,
186
+ limit: int = 1000
187
+ ) -> Dict[str, Any]:
188
+ response = self._request(
189
+ 'GET',
190
+ f'/analysis/{self._team_id}/trajectory/{trajectory_id}',
191
+ params={'page': page, 'limit': limit}
192
+ )
193
+
194
+ return response.get('data')
195
+
196
+ def list_analysis_results(
197
+ self,
198
+ analysis_id: str,
199
+ page: int = 1,
200
+ limit: int = 1000
201
+ ):
202
+ response = self._request(
203
+ 'GET',
204
+ f'/plugin/{self._team_id}/listing/analysis/{analysis_id}',
205
+ params={'page': page, 'limit': limit}
206
+ )
207
+
208
+ return response.get('data')
209
+
210
+ def find_analysis_by_id(self, analysis_id: str):
211
+ return self._request(
212
+ 'GET',
213
+ f'/analysis/{self._team_id}/{analysis_id}'
214
+ )
215
+
216
+ def _resolve_trajectory_id(self, analysis_id: str) -> str:
217
+ analysis = self.find_analysis_by_id(analysis_id)
218
+ trajectory_id = analysis.get('trajectory')
219
+ if not trajectory_id:
220
+ raise RuntimeError('analysis trajectory not found')
221
+
222
+ return trajectory_id
223
+
224
+ def download_analysis_artifacts(
225
+ self,
226
+ analysis_id: str,
227
+ unzip: bool = True
228
+ ):
229
+ zip_path = self._download_stream(
230
+ f'/plugin/{self._team_id}/exposure/export/analysis/{analysis_id}',
231
+ f'analysis-{analysis_id}-artifacts.zip'
232
+ )
233
+
234
+ if unzip:
235
+ return self._unzip_recursive(zip_path)
236
+
237
+ return zip_path
238
+
239
+ def download_plugin_results_file(
240
+ self,
241
+ analysis_id: str,
242
+ unzip: bool = True
243
+ ):
244
+ return self.download_analysis_artifacts(analysis_id, unzip=unzip)
245
+
246
+ def download_frame_glb(
247
+ self,
248
+ analysis_id: str,
249
+ timestep: int
250
+ ):
251
+ trajectory_id = self._resolve_trajectory_id(analysis_id)
252
+
253
+ return self._download_stream(
254
+ f'/trajectory/{self._team_id}/{trajectory_id}/{timestep}/{analysis_id}',
255
+ f'frame-{analysis_id}-{timestep}.glb'
256
+ )
257
+
258
+ def download_plugin_exported_glb(
259
+ self,
260
+ analysis_id: str,
261
+ exposure_id: str,
262
+ timestep: int
263
+ ):
264
+ trajectory_id = self._resolve_trajectory_id(analysis_id)
265
+
266
+ return self._download_stream(
267
+ f'/plugin/{self._team_id}/exposure/glb/{trajectory_id}/{analysis_id}/{exposure_id}/{timestep}',
268
+ f'plugin-glb-{analysis_id}-{exposure_id}-{timestep}.glb'
269
+ )
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "voltsdk"
7
+ version = "0.1.0"
8
+ description = "Volt API SDK for scripting and notebooks"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "msgpack>=1.0.0",
13
+ "pandas>=2.0.0",
14
+ "requests>=2.31.0"
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ visualization = [
19
+ "vtk>=9.2.0"
20
+ ]
21
+ notebook = [
22
+ "vtk>=9.2.0",
23
+ "k3d>=2.16.0"
24
+ ]
25
+
26
+ [tool.setuptools]
27
+ packages = ["voltsdk"]
28
+
29
+ [tool.setuptools.package-dir]
30
+ voltsdk = "."
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
voltsdk-0.1.0/utils.py ADDED
@@ -0,0 +1,154 @@
1
+ from typing import Any, Optional
2
+ import msgpack
3
+ import os
4
+ import pandas as pd
5
+
6
+
7
+ def get_nested_value(data: Any, path: Optional[str]) -> Any:
8
+ if not path:
9
+ return data
10
+
11
+ current = data
12
+ for key in path.split('.'):
13
+ if not isinstance(current, dict):
14
+ return None
15
+
16
+ if key not in current:
17
+ return None
18
+
19
+ current = current[key]
20
+
21
+ return current
22
+
23
+
24
+ def merged_chunked_value(target: Any, incoming: Any) -> Any:
25
+ if incoming is None:
26
+ return target
27
+
28
+ if target is None:
29
+ return incoming
30
+
31
+ if isinstance(target, list) and isinstance(incoming, list):
32
+ target.extend(incoming)
33
+ return target
34
+
35
+ if isinstance(target, dict) and isinstance(incoming, dict):
36
+ for key, incoming_value in incoming.items():
37
+ target_value = target.get(key)
38
+
39
+ if isinstance(target_value, list) and isinstance(incoming_value, list):
40
+ target_value.extend(incoming_value)
41
+ elif isinstance(target_value, dict) and isinstance(incoming_value, dict):
42
+ target[key] = merged_chunked_value(target_value, incoming_value)
43
+ else:
44
+ target[key] = incoming_value
45
+ return target
46
+
47
+ return incoming
48
+
49
+
50
+ def is_columnar_dict(value: Any) -> bool:
51
+ if not isinstance(value, dict) or not value:
52
+ return False
53
+
54
+ lengths = []
55
+ for item in value.values():
56
+ if not isinstance(item, list):
57
+ return False
58
+ lengths.append(len(item))
59
+ return len(set(lengths)) == 1
60
+
61
+
62
+ def msgpack_as_df(file_path: str, iterable_key: Optional[str] = None):
63
+ if not os.path.exists(file_path):
64
+ raise RuntimeError(f'file not found: {file_path}')
65
+
66
+ data = None
67
+ with open(file_path, 'rb') as file:
68
+ unpacker = msgpack.Unpacker(file, raw=False)
69
+ for message in unpacker:
70
+ chunk = get_nested_value(message, iterable_key)
71
+ data = merged_chunked_value(data, chunk)
72
+
73
+ if data is None:
74
+ return pd.DataFrame()
75
+
76
+ if isinstance(data, list) or is_columnar_dict(data):
77
+ return pd.DataFrame(data)
78
+
79
+ if isinstance(data, dict):
80
+ return pd.DataFrame([data])
81
+
82
+ return pd.DataFrame([{'value': data}])
83
+
84
+
85
+ def view_glb(file_path: str):
86
+ if not os.path.exists(file_path):
87
+ raise RuntimeError(f'file not found: {file_path}')
88
+
89
+ try:
90
+ import vtk
91
+ except Exception as error:
92
+ raise RuntimeError(
93
+ 'vtk is required to view GLB files. Install with: pip install "voltsdk[visualization]"'
94
+ ) from error
95
+
96
+ reader = vtk.vtkGLTFReader()
97
+ reader.SetFileName(file_path)
98
+ reader.Update()
99
+
100
+ in_notebook = False
101
+ try:
102
+ from IPython import get_ipython
103
+ shell = get_ipython()
104
+ in_notebook = shell is not None and shell.__class__.__name__ == 'ZMQInteractiveShell'
105
+ except Exception:
106
+ in_notebook = False
107
+
108
+ if in_notebook:
109
+ try:
110
+ import k3d
111
+ except Exception as error:
112
+ raise RuntimeError('k3d is required in notebook mode. Install with: pip install k3d') from error
113
+
114
+ plot = k3d.plot()
115
+ multi_block = reader.GetOutput()
116
+ iterator = multi_block.NewIterator()
117
+ iterator.InitTraversal()
118
+ while not iterator.IsDoneWithTraversal():
119
+ item = iterator.GetCurrentDataObject()
120
+ if item is not None:
121
+ plot += k3d.vtk_poly_data(item, color=0x222222)
122
+ iterator.GoToNextItem()
123
+
124
+ try:
125
+ from IPython.display import display as ipython_display
126
+ ipython_display(plot)
127
+ except Exception:
128
+ pass
129
+
130
+ return plot
131
+
132
+ mapper = vtk.vtkCompositePolyDataMapper2()
133
+ mapper.SetInputDataObject(reader.GetOutput())
134
+
135
+ actor = vtk.vtkActor()
136
+ actor.SetMapper(mapper)
137
+
138
+ renderer = vtk.vtkRenderer()
139
+ renderer.AddActor(actor)
140
+ renderer.SetBackground(0.07, 0.07, 0.07)
141
+
142
+ render_window = vtk.vtkRenderWindow()
143
+ render_window.AddRenderer(renderer)
144
+ render_window.SetSize(1200, 800)
145
+ render_window.SetWindowName(os.path.basename(file_path))
146
+
147
+ interactor = vtk.vtkRenderWindowInteractor()
148
+ interactor.SetRenderWindow(render_window)
149
+
150
+ render_window.Render()
151
+ interactor.Initialize()
152
+ interactor.Start()
153
+
154
+ return interactor
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: voltsdk
3
+ Version: 0.1.0
4
+ Summary: Volt API SDK for scripting and notebooks
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: msgpack>=1.0.0
8
+ Requires-Dist: pandas>=2.0.0
9
+ Requires-Dist: requests>=2.31.0
10
+ Provides-Extra: visualization
11
+ Requires-Dist: vtk>=9.2.0; extra == "visualization"
12
+ Provides-Extra: notebook
13
+ Requires-Dist: vtk>=9.2.0; extra == "notebook"
14
+ Requires-Dist: k3d>=2.16.0; extra == "notebook"
15
+
16
+ # voltsdk
17
+
18
+ Python SDK for interacting with the Volt API.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install voltsdk
24
+ ```
25
+
26
+ Install visualization support with:
27
+
28
+ ```bash
29
+ pip install "voltsdk[visualization]"
30
+ ```
31
+
32
+ Install notebook support with:
33
+
34
+ ```bash
35
+ pip install "voltsdk[notebook]"
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ from voltsdk import VoltClient
42
+
43
+ client = VoltClient(
44
+ secret_key="your-secret-key",
45
+ base_url="https://api.example.com"
46
+ )
47
+ ```
48
+
@@ -0,0 +1,13 @@
1
+ README.md
2
+ __init__.py
3
+ client.py
4
+ pyproject.toml
5
+ utils.py
6
+ ./__init__.py
7
+ ./client.py
8
+ ./utils.py
9
+ voltsdk.egg-info/PKG-INFO
10
+ voltsdk.egg-info/SOURCES.txt
11
+ voltsdk.egg-info/dependency_links.txt
12
+ voltsdk.egg-info/requires.txt
13
+ voltsdk.egg-info/top_level.txt
@@ -0,0 +1,10 @@
1
+ msgpack>=1.0.0
2
+ pandas>=2.0.0
3
+ requests>=2.31.0
4
+
5
+ [notebook]
6
+ vtk>=9.2.0
7
+ k3d>=2.16.0
8
+
9
+ [visualization]
10
+ vtk>=9.2.0
@@ -0,0 +1 @@
1
+ voltsdk