luminarycloud 0.16.2__py3-none-any.whl → 0.18.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.
- luminarycloud/_auth/auth.py +23 -34
- luminarycloud/_client/client.py +23 -4
- luminarycloud/_client/retry_interceptor.py +7 -0
- luminarycloud/_helpers/_create_geometry.py +134 -34
- luminarycloud/_helpers/_wait_for_mesh.py +14 -4
- luminarycloud/_helpers/cond.py +0 -1
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +146 -123
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +82 -15
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.py +8 -8
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.pyi +12 -7
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.py +25 -3
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.pyi +30 -0
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2.py +246 -0
- luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2.pyi +420 -0
- luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2_grpc.py +240 -0
- luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2_grpc.pyi +90 -0
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.py +54 -3
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.pyi +92 -1
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2_grpc.py +132 -0
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2_grpc.pyi +40 -0
- luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2.py +97 -0
- luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2.pyi +93 -0
- luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2_grpc.py +132 -0
- luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2_grpc.pyi +44 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.py +48 -26
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.pyi +30 -2
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2_grpc.py +36 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2_grpc.pyi +18 -0
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.py +153 -133
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.pyi +51 -3
- luminarycloud/_proto/client/simulation_pb2.py +261 -251
- luminarycloud/_proto/client/simulation_pb2.pyi +35 -2
- luminarycloud/_proto/frontend/output/output_pb2.py +24 -24
- luminarycloud/_proto/frontend/output/output_pb2.pyi +6 -3
- luminarycloud/_proto/geometry/geometry_pb2.py +63 -63
- luminarycloud/_proto/geometry/geometry_pb2.pyi +16 -8
- luminarycloud/_proto/hexmesh/hexmesh_pb2.py +17 -4
- luminarycloud/_proto/hexmesh/hexmesh_pb2.pyi +22 -1
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.py +10 -10
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.pyi +12 -7
- luminarycloud/_proto/quantity/quantity_pb2.py +19 -19
- luminarycloud/enum/geometry_status.py +15 -8
- luminarycloud/enum/pipeline_job_status.py +23 -0
- luminarycloud/feature_modification.py +3 -6
- luminarycloud/geometry.py +25 -0
- luminarycloud/geometry_version.py +23 -0
- luminarycloud/mesh.py +16 -0
- luminarycloud/params/enum/_enum_wrappers.py +29 -0
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/fan_curve_inlet_.py +1 -1
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/mach_inlet_.py +5 -1
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/mass_flow_inlet_.py +5 -1
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/total_pressure_inlet_.py +5 -1
- luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/velocity_magnitude_inlet_.py +5 -1
- luminarycloud/physics_ai/inference.py +46 -30
- luminarycloud/pipelines/__init__.py +7 -0
- luminarycloud/pipelines/api.py +213 -0
- luminarycloud/project.py +98 -11
- luminarycloud/simulation_template.py +15 -6
- luminarycloud/vis/__init__.py +6 -0
- luminarycloud/vis/data_extraction.py +201 -31
- luminarycloud/vis/filters.py +94 -35
- luminarycloud/vis/interactive_inference.py +153 -0
- luminarycloud/vis/interactive_scene.py +35 -16
- luminarycloud/vis/primitives.py +87 -1
- luminarycloud/vis/visualization.py +50 -6
- luminarycloud/volume_selection.py +3 -6
- {luminarycloud-0.16.2.dist-info → luminarycloud-0.18.0.dist-info}/METADATA +18 -18
- {luminarycloud-0.16.2.dist-info → luminarycloud-0.18.0.dist-info}/RECORD +73 -62
- {luminarycloud-0.16.2.dist-info → luminarycloud-0.18.0.dist-info}/WHEEL +0 -0
luminarycloud/_auth/auth.py
CHANGED
|
@@ -38,9 +38,10 @@ class Auth0Client:
|
|
|
38
38
|
(Optional) Auth0 client ID
|
|
39
39
|
audience : str
|
|
40
40
|
(Optional) Auth0 audience (i.e. target service ID)
|
|
41
|
-
|
|
42
|
-
(Optional) If True, prevents client from
|
|
43
|
-
|
|
41
|
+
noninteractive : bool
|
|
42
|
+
(Optional) If True, prevents client from ever attempting an interactive login (i.e.
|
|
43
|
+
launching an http server and opening the login page in a web browser). In cases where user
|
|
44
|
+
interaction is required, an error will be raised. Default: False
|
|
44
45
|
access_token : str
|
|
45
46
|
(Optional) Auth0 access token.
|
|
46
47
|
refresh_token : str
|
|
@@ -55,7 +56,7 @@ class Auth0Client:
|
|
|
55
56
|
domain: Optional[str] = None,
|
|
56
57
|
client_id: Optional[str] = None,
|
|
57
58
|
audience: Optional[str] = None,
|
|
58
|
-
|
|
59
|
+
noninteractive: bool = False,
|
|
59
60
|
access_token: Optional[str] = None,
|
|
60
61
|
refresh_token: Optional[str] = None,
|
|
61
62
|
refresh_rotation: Optional[bool] = None,
|
|
@@ -63,7 +64,7 @@ class Auth0Client:
|
|
|
63
64
|
self.domain = domain or LC_AUTH_DOMAIN # Auth0 tenant domain
|
|
64
65
|
self.client_id = client_id or LC_AUTH_CLIENT_ID # Auth0 ID for client
|
|
65
66
|
self.audience = audience or LC_AUTH_SERVICE_ID # Auth0 ID for service
|
|
66
|
-
self.
|
|
67
|
+
self.noninteractive = noninteractive # True to prevent interactive login
|
|
67
68
|
self.refresh_rotation = (
|
|
68
69
|
refresh_rotation or LC_REFRESH_ROTATION
|
|
69
70
|
) # True if refresh token rotation is enabled on Auth0
|
|
@@ -187,12 +188,16 @@ class Auth0Client:
|
|
|
187
188
|
Raises
|
|
188
189
|
------
|
|
189
190
|
InteractiveAuthException
|
|
190
|
-
If user interaction is required, and self.
|
|
191
|
+
If user interaction is required, and self.noninteractive is True.
|
|
191
192
|
SecurityAlertException
|
|
192
193
|
If the callback server receives a request from a potential attacker.
|
|
193
194
|
AuthException
|
|
194
195
|
If the tokens received from the Auth0 exchange are not found in the response.
|
|
195
196
|
"""
|
|
197
|
+
if self.noninteractive:
|
|
198
|
+
raise InteractiveAuthException(
|
|
199
|
+
"User interaction disabled, cannot initiate browser login."
|
|
200
|
+
)
|
|
196
201
|
|
|
197
202
|
logger.info("Initiating user login.")
|
|
198
203
|
|
|
@@ -226,34 +231,18 @@ class Auth0Client:
|
|
|
226
231
|
url_parameters["redirect_uri"] = redirect_uri
|
|
227
232
|
authorize_url = urljoin(self.base_url, "authorize?" + urlencode(url_parameters))
|
|
228
233
|
|
|
229
|
-
logger.info("
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
# Fall back to interactive auth if silent auth fails
|
|
243
|
-
if "error" in callback_args:
|
|
244
|
-
if self.no_browser:
|
|
245
|
-
raise InteractiveAuthException(
|
|
246
|
-
"Cannot login without user interaction via browser."
|
|
247
|
-
)
|
|
248
|
-
logger.info("Silent auth failed. Prompting interactive login via browser.")
|
|
249
|
-
webbrowser.open_new(authorize_url)
|
|
250
|
-
print(
|
|
251
|
-
"Interactive login required. Your browser has been opened to visit the following URL:\n\n",
|
|
252
|
-
authorize_url,
|
|
253
|
-
"\n",
|
|
254
|
-
)
|
|
255
|
-
logger.debug("Waiting for redirect callback.")
|
|
256
|
-
callback_args = listener.block_until_callback()
|
|
234
|
+
logger.info("Prompting interactive login via browser.")
|
|
235
|
+
if webbrowser.open_new(authorize_url):
|
|
236
|
+
interaction_detail = "Your browser has been opened to visit the following URL"
|
|
237
|
+
else:
|
|
238
|
+
interaction_detail = "Please visit the following URL in your browser"
|
|
239
|
+
print(
|
|
240
|
+
f"Interactive login required. {interaction_detail}:\n\n",
|
|
241
|
+
authorize_url,
|
|
242
|
+
"\n",
|
|
243
|
+
)
|
|
244
|
+
logger.debug("Waiting for redirect callback.")
|
|
245
|
+
callback_args = listener.block_until_callback()
|
|
257
246
|
|
|
258
247
|
if "error" in callback_args:
|
|
259
248
|
raise AuthException(callback_args["error"] + ": " + callback_args["error_description"])
|
luminarycloud/_client/client.py
CHANGED
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
import atexit
|
|
5
5
|
from contextvars import ContextVar, Token
|
|
6
6
|
from collections.abc import Iterable
|
|
7
|
-
from typing import Any, Optional
|
|
7
|
+
from typing import Any, Optional, Union
|
|
8
8
|
|
|
9
9
|
import grpc
|
|
10
10
|
|
|
@@ -28,6 +28,7 @@ from .._proto.api.v0.luminarycloud.simulation_template.simulation_template_pb2_g
|
|
|
28
28
|
from .._proto.api.v0.luminarycloud.named_variable_set.named_variable_set_pb2_grpc import (
|
|
29
29
|
NamedVariableSetServiceStub,
|
|
30
30
|
)
|
|
31
|
+
from .._proto.api.v0.luminarycloud.pipelines.pipelines_pb2_grpc import PipelineServiceStub
|
|
31
32
|
from .._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2_grpc import (
|
|
32
33
|
PhysicsAiServiceStub,
|
|
33
34
|
)
|
|
@@ -35,6 +36,9 @@ from .._proto.api.v0.luminarycloud.inference.inference_pb2_grpc import Inference
|
|
|
35
36
|
from .._proto.api.v0.luminarycloud.thirdpartyintegration.onshape.onshape_pb2_grpc import (
|
|
36
37
|
OnshapeServiceStub,
|
|
37
38
|
)
|
|
39
|
+
from .._proto.api.v0.luminarycloud.project_ui_state.project_ui_state_pb2_grpc import (
|
|
40
|
+
ProjectUIStateServiceStub,
|
|
41
|
+
)
|
|
38
42
|
from .._proto.api.v0.luminarycloud.solution.solution_pb2_grpc import SolutionServiceStub
|
|
39
43
|
from .._proto.api.v0.luminarycloud.upload.upload_pb2_grpc import UploadServiceStub
|
|
40
44
|
from .._proto.api.v0.luminarycloud.vis.vis_pb2_grpc import VisAPIServiceStub
|
|
@@ -61,9 +65,11 @@ class Client(
|
|
|
61
65
|
OutputDefinitionServiceStub,
|
|
62
66
|
StoppingConditionServiceStub,
|
|
63
67
|
NamedVariableSetServiceStub,
|
|
68
|
+
PipelineServiceStub,
|
|
64
69
|
PhysicsAiServiceStub,
|
|
65
70
|
InferenceServiceStub,
|
|
66
71
|
OnshapeServiceStub,
|
|
72
|
+
ProjectUIStateServiceStub,
|
|
67
73
|
):
|
|
68
74
|
"""
|
|
69
75
|
Creates a Luminary API client.
|
|
@@ -102,7 +108,7 @@ class Client(
|
|
|
102
108
|
self,
|
|
103
109
|
target: str = LC_DOMAIN,
|
|
104
110
|
localhost: bool = False,
|
|
105
|
-
grpc_channel_options: Optional[Iterable[tuple[str, str]]] = None,
|
|
111
|
+
grpc_channel_options: Optional[Iterable[tuple[str, Union[str, int]]]] = None,
|
|
106
112
|
channel_credentials: Optional[grpc.ChannelCredentials] = None,
|
|
107
113
|
api_key: Optional[str] = LC_API_KEY,
|
|
108
114
|
**kwargs: Any,
|
|
@@ -111,8 +117,19 @@ class Client(
|
|
|
111
117
|
self._apiserver_domain = target.split(":", maxsplit=1)[0]
|
|
112
118
|
# Initialize Auth0 client only if not using API key
|
|
113
119
|
self._auth0_client = None if api_key else Auth0Client(**kwargs)
|
|
120
|
+
# It seems that both python and golang cliens have trouble sometimes RPC calls getting
|
|
121
|
+
# stuck. In go, setting some keepalive options seems to help, so we'll do the same here. See
|
|
122
|
+
# https://github.com/grpc/grpc/blob/d8b7d55975b945a9dee40db5ee87f170590721d9/examples/python/keep_alive/greeter_client.py#L1.
|
|
123
|
+
grpc_channel_options_with_keep_alive: list[tuple[str, Union[str, int]]] = [
|
|
124
|
+
("grpc.keepalive_time_ms", 50000),
|
|
125
|
+
("grpc.keepalive_timeout_ms", 5000),
|
|
126
|
+
("grpc.keepalive_permit_without_calls", 1),
|
|
127
|
+
("grpc.http2.max_pings_without_data", 10),
|
|
128
|
+
]
|
|
129
|
+
if grpc_channel_options:
|
|
130
|
+
grpc_channel_options_with_keep_alive.extend(grpc_channel_options)
|
|
114
131
|
self._channel = self._create_channel(
|
|
115
|
-
localhost,
|
|
132
|
+
localhost, grpc_channel_options_with_keep_alive, channel_credentials, api_key
|
|
116
133
|
)
|
|
117
134
|
self._context_tokens: list[Token] = []
|
|
118
135
|
self.__register_rpcs()
|
|
@@ -157,7 +174,7 @@ class Client(
|
|
|
157
174
|
def _create_channel(
|
|
158
175
|
self,
|
|
159
176
|
localhost: bool = False,
|
|
160
|
-
grpc_channel_options: Optional[Iterable[tuple[str, str]]] = None,
|
|
177
|
+
grpc_channel_options: Optional[Iterable[tuple[str, Union[str, int]]]] = None,
|
|
161
178
|
channel_credentials: Optional[grpc.ChannelCredentials] = None,
|
|
162
179
|
api_key: Optional[str] = None,
|
|
163
180
|
) -> grpc.Channel:
|
|
@@ -207,10 +224,12 @@ class Client(
|
|
|
207
224
|
OutputNodeServiceStub.__init__(self, self._channel)
|
|
208
225
|
OutputDefinitionServiceStub.__init__(self, self._channel)
|
|
209
226
|
StoppingConditionServiceStub.__init__(self, self._channel)
|
|
227
|
+
PipelineServiceStub.__init__(self, self._channel)
|
|
210
228
|
PhysicsAiServiceStub.__init__(self, self._channel)
|
|
211
229
|
InferenceServiceStub.__init__(self, self._channel)
|
|
212
230
|
NamedVariableSetServiceStub.__init__(self, self._channel)
|
|
213
231
|
OnshapeServiceStub.__init__(self, self._channel)
|
|
232
|
+
ProjectUIStateServiceStub.__init__(self, self._channel)
|
|
214
233
|
for name, value in self.__dict__.items():
|
|
215
234
|
if isinstance(value, grpc.UnaryUnaryMultiCallable):
|
|
216
235
|
setattr(self, name, rpc_error(value))
|
|
@@ -47,6 +47,13 @@ class RetryInterceptor(UnaryUnaryClientInterceptor):
|
|
|
47
47
|
call = continuation(client_call_details, request)
|
|
48
48
|
if call.code() not in retryable_codes:
|
|
49
49
|
break
|
|
50
|
+
if call.code() == grpc.StatusCode.UNAVAILABLE:
|
|
51
|
+
# if the auth plugin errors, that unfortunately shows up here as UNAVAILABLE, so we
|
|
52
|
+
# have to check for auth plugin exceptions that shouldn't be retried by matching
|
|
53
|
+
# their name in the details string
|
|
54
|
+
details = call.details() or ""
|
|
55
|
+
if "InteractiveAuthException" in details:
|
|
56
|
+
break
|
|
50
57
|
sleep(backoff)
|
|
51
58
|
try:
|
|
52
59
|
call.result()
|
|
@@ -1,21 +1,152 @@
|
|
|
1
1
|
# Copyright 2024 Luminary Cloud, Inc. All Rights Reserved.
|
|
2
2
|
from luminarycloud._proto.api.v0.luminarycloud.geometry import geometry_pb2 as geometrypb
|
|
3
3
|
from luminarycloud._proto.upload import upload_pb2 as uploadpb
|
|
4
|
+
from luminarycloud.types.adfloat import _to_ad_proto
|
|
4
5
|
from os import PathLike
|
|
5
6
|
from .._client import Client
|
|
6
7
|
from .upload import upload_file
|
|
7
|
-
from typing import Optional
|
|
8
|
+
from typing import List, Optional
|
|
8
9
|
from luminarycloud._helpers import util
|
|
9
10
|
import uuid
|
|
10
11
|
import random
|
|
11
12
|
import time
|
|
13
|
+
import tempfile
|
|
14
|
+
import zipfile
|
|
15
|
+
from pathlib import Path
|
|
12
16
|
|
|
13
17
|
import logging
|
|
14
18
|
|
|
15
19
|
logger = logging.getLogger(__name__)
|
|
16
20
|
|
|
17
21
|
|
|
22
|
+
def _create_zip(file_paths: List[PathLike | str]) -> Path:
|
|
23
|
+
"""Create ZIP file."""
|
|
24
|
+
|
|
25
|
+
zip_path = Path(tempfile.mktemp(suffix=".zip"))
|
|
26
|
+
|
|
27
|
+
with zipfile.ZipFile(zip_path, "w") as zf:
|
|
28
|
+
for file_path in file_paths:
|
|
29
|
+
path = Path(file_path)
|
|
30
|
+
zf.write(path, path.name) # Flatten structure
|
|
31
|
+
|
|
32
|
+
return zip_path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _create_geometry_from_url(
|
|
36
|
+
client: Client,
|
|
37
|
+
project_id: str,
|
|
38
|
+
url: str,
|
|
39
|
+
name: str,
|
|
40
|
+
web_geometry_id: str,
|
|
41
|
+
scaling: Optional[float],
|
|
42
|
+
wait: bool,
|
|
43
|
+
) -> geometrypb.Geometry:
|
|
44
|
+
"""Create geometry from already-uploaded URL (shared logic)."""
|
|
45
|
+
|
|
46
|
+
if scaling is None:
|
|
47
|
+
# default to no scaling
|
|
48
|
+
scaling = 1.0
|
|
49
|
+
|
|
50
|
+
create_geo_res: geometrypb.CreateGeometryResponse = client.CreateGeometry(
|
|
51
|
+
geometrypb.CreateGeometryRequest(
|
|
52
|
+
project_id=project_id,
|
|
53
|
+
name=name,
|
|
54
|
+
url=url,
|
|
55
|
+
web_geometry_id=web_geometry_id,
|
|
56
|
+
scaling=_to_ad_proto(scaling),
|
|
57
|
+
wait=False,
|
|
58
|
+
request_id=str(uuid.uuid4()),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
geo = create_geo_res.geometry
|
|
62
|
+
|
|
63
|
+
# Prefer polling on the client than waiting on the server (although waiting on the server
|
|
64
|
+
# notifies the clients potentially faster).
|
|
65
|
+
if wait:
|
|
66
|
+
last_version_id = ""
|
|
67
|
+
while not last_version_id:
|
|
68
|
+
jitter = random.uniform(0.5, 1.5)
|
|
69
|
+
time.sleep(2 + jitter)
|
|
70
|
+
req = geometrypb.GetGeometryRequest(geometry_id=create_geo_res.geometry.id)
|
|
71
|
+
res_geo: geometrypb.GetGeometryResponse = client.GetGeometry(req)
|
|
72
|
+
geo = res_geo.geometry
|
|
73
|
+
last_version_id = geo.last_version_id
|
|
74
|
+
|
|
75
|
+
logger.info(f"created geometry {geo.name} ({geo.id})")
|
|
76
|
+
return geo
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _create_geometry_from_multiple_files(
|
|
80
|
+
client: Client,
|
|
81
|
+
cad_file_paths: List[PathLike | str],
|
|
82
|
+
project_id: str,
|
|
83
|
+
*,
|
|
84
|
+
name: Optional[str] = None,
|
|
85
|
+
scaling: Optional[float] = None,
|
|
86
|
+
wait: bool = False,
|
|
87
|
+
) -> geometrypb.Geometry:
|
|
88
|
+
"""
|
|
89
|
+
Create geometry from multiple files using frontend's proven pattern.
|
|
90
|
+
|
|
91
|
+
Creates a ZIP file and uploads using meshParams (not geometryParams)
|
|
92
|
+
to leverage existing FileSetManifest infrastructure.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# Create ZIP file (simple, no validation - like single-file)
|
|
96
|
+
zip_path = _create_zip(cad_file_paths)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# Upload ZIP using meshParams
|
|
100
|
+
finish_res = upload_file(
|
|
101
|
+
client,
|
|
102
|
+
project_id,
|
|
103
|
+
uploadpb.ResourceParams(mesh_params=uploadpb.MeshParams(scaling=scaling or 1.0)),
|
|
104
|
+
zip_path,
|
|
105
|
+
)[1]
|
|
106
|
+
|
|
107
|
+
# Create geometry from uploaded ZIP URL
|
|
108
|
+
return _create_geometry_from_url(
|
|
109
|
+
client,
|
|
110
|
+
project_id,
|
|
111
|
+
finish_res.url,
|
|
112
|
+
name or "Multi-file Geometry",
|
|
113
|
+
"",
|
|
114
|
+
scaling,
|
|
115
|
+
wait,
|
|
116
|
+
)
|
|
117
|
+
finally:
|
|
118
|
+
# Clean up ZIP
|
|
119
|
+
zip_path.unlink()
|
|
120
|
+
|
|
121
|
+
|
|
18
122
|
def create_geometry(
|
|
123
|
+
client: Client,
|
|
124
|
+
cad_file_path: PathLike | str | List[PathLike | str],
|
|
125
|
+
project_id: str,
|
|
126
|
+
*,
|
|
127
|
+
name: Optional[str] = None,
|
|
128
|
+
scaling: Optional[float] = None,
|
|
129
|
+
wait: bool = False,
|
|
130
|
+
) -> geometrypb.Geometry:
|
|
131
|
+
"""
|
|
132
|
+
Create a geometry from single or multiple CAD files.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
# Route to appropriate handler based on input type
|
|
136
|
+
if isinstance(cad_file_path, (list, tuple)) and len(cad_file_path) > 1:
|
|
137
|
+
# Multi-file: use mesh upload pattern (like frontend)
|
|
138
|
+
return _create_geometry_from_multiple_files(
|
|
139
|
+
client, cad_file_path, project_id, name=name, scaling=scaling, wait=wait
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Single file: existing logic
|
|
143
|
+
single_path = cad_file_path[0] if isinstance(cad_file_path, (list, tuple)) else cad_file_path
|
|
144
|
+
return _create_geometry_from_single_file(
|
|
145
|
+
client, single_path, project_id, name=name, scaling=scaling, wait=wait
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _create_geometry_from_single_file(
|
|
19
150
|
client: Client,
|
|
20
151
|
cad_file_path: PathLike | str,
|
|
21
152
|
project_id: str,
|
|
@@ -23,8 +154,8 @@ def create_geometry(
|
|
|
23
154
|
name: Optional[str] = None,
|
|
24
155
|
scaling: Optional[float] = None,
|
|
25
156
|
wait: bool = False,
|
|
26
|
-
convert_to_discrete: bool = False,
|
|
27
157
|
) -> geometrypb.Geometry:
|
|
158
|
+
"""Create geometry from single file."""
|
|
28
159
|
|
|
29
160
|
# TODO(onshape): Document this publicly when we release
|
|
30
161
|
cad_file_path_str = str(cad_file_path)
|
|
@@ -62,35 +193,4 @@ def create_geometry(
|
|
|
62
193
|
# if the caller did not provide a name, use the file name
|
|
63
194
|
name = cad_file_meta.name
|
|
64
195
|
|
|
65
|
-
|
|
66
|
-
# default to no scaling
|
|
67
|
-
scaling = 1.0
|
|
68
|
-
|
|
69
|
-
create_geo_res: geometrypb.CreateGeometryResponse = client.CreateGeometry(
|
|
70
|
-
geometrypb.CreateGeometryRequest(
|
|
71
|
-
project_id=project_id,
|
|
72
|
-
name=name,
|
|
73
|
-
url=url,
|
|
74
|
-
web_geometry_id=web_geometry_id,
|
|
75
|
-
scaling=scaling,
|
|
76
|
-
wait=False,
|
|
77
|
-
request_id=str(uuid.uuid4()),
|
|
78
|
-
force_discrete=convert_to_discrete,
|
|
79
|
-
)
|
|
80
|
-
)
|
|
81
|
-
geo = create_geo_res.geometry
|
|
82
|
-
|
|
83
|
-
# Prefer polling on the client than waiting on the server (although waiting on the server
|
|
84
|
-
# notifies the clients potentially faster).
|
|
85
|
-
if wait:
|
|
86
|
-
last_version_id = ""
|
|
87
|
-
while not last_version_id:
|
|
88
|
-
jitter = random.uniform(0.5, 1.5)
|
|
89
|
-
time.sleep(2 + jitter)
|
|
90
|
-
req = geometrypb.GetGeometryRequest(geometry_id=create_geo_res.geometry.id)
|
|
91
|
-
res_geo: geometrypb.GetGeometryResponse = client.GetGeometry(req)
|
|
92
|
-
geo = res_geo.geometry
|
|
93
|
-
last_version_id = geo.last_version_id
|
|
94
|
-
|
|
95
|
-
logger.info(f"created geometry {geo.name} ({geo.id})")
|
|
96
|
-
return geo
|
|
196
|
+
return _create_geometry_from_url(client, project_id, url, name, web_geometry_id, scaling, wait)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright 2023-2024 Luminary Cloud, Inc. All Rights Reserved.
|
|
2
2
|
import logging
|
|
3
3
|
from time import time, sleep
|
|
4
|
+
import grpc
|
|
4
5
|
|
|
5
6
|
from .._proto.api.v0.luminarycloud.mesh.mesh_pb2 import Mesh, GetMeshRequest
|
|
6
7
|
from .._client import Client
|
|
@@ -35,11 +36,20 @@ def wait_for_mesh(
|
|
|
35
36
|
"""
|
|
36
37
|
deadline = time() + timeout_seconds
|
|
37
38
|
while True:
|
|
38
|
-
|
|
39
|
+
if time() >= deadline:
|
|
40
|
+
logger.error("`wait_for_mesh` timed out.")
|
|
41
|
+
raise TimeoutError
|
|
42
|
+
# It seems this call sometimes hangs. It seems as well that python gRPC is known to hang
|
|
43
|
+
# in some cases, so we'll add a deadline and catch deadline exceeded errors to retry.
|
|
44
|
+
try:
|
|
45
|
+
response = client.GetMesh(GetMeshRequest(id=mesh.id), timeout=15)
|
|
46
|
+
except grpc.RpcError as e:
|
|
47
|
+
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
|
|
48
|
+
logger.error("Deadline exceeded while waiting for mesh.")
|
|
49
|
+
sleep(max(0, min(interval_seconds, deadline - time())))
|
|
50
|
+
continue
|
|
51
|
+
raise e
|
|
39
52
|
status = response.mesh.status
|
|
40
53
|
if status in [Mesh.MESH_STATUS_COMPLETED, Mesh.MESH_STATUS_FAILED]:
|
|
41
54
|
return status
|
|
42
55
|
sleep(max(0, min(interval_seconds, deadline - time())))
|
|
43
|
-
if time() >= deadline:
|
|
44
|
-
logger.error("`wait_for_mesh` timed out.")
|
|
45
|
-
raise TimeoutError
|
luminarycloud/_helpers/cond.py
CHANGED