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.
Files changed (73) hide show
  1. luminarycloud/_auth/auth.py +23 -34
  2. luminarycloud/_client/client.py +23 -4
  3. luminarycloud/_client/retry_interceptor.py +7 -0
  4. luminarycloud/_helpers/_create_geometry.py +134 -34
  5. luminarycloud/_helpers/_wait_for_mesh.py +14 -4
  6. luminarycloud/_helpers/cond.py +0 -1
  7. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +146 -123
  8. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +82 -15
  9. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.py +34 -0
  10. luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.pyi +12 -0
  11. luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.py +8 -8
  12. luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.pyi +12 -7
  13. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.py +25 -3
  14. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.pyi +30 -0
  15. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2_grpc.py +34 -0
  16. luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2_grpc.pyi +12 -0
  17. luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2.py +246 -0
  18. luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2.pyi +420 -0
  19. luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2_grpc.py +240 -0
  20. luminarycloud/_proto/api/v0/luminarycloud/pipelines/pipelines_pb2_grpc.pyi +90 -0
  21. luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.py +54 -3
  22. luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.pyi +92 -1
  23. luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2_grpc.py +132 -0
  24. luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2_grpc.pyi +40 -0
  25. luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2.py +97 -0
  26. luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2.pyi +93 -0
  27. luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2_grpc.py +132 -0
  28. luminarycloud/_proto/api/v0/luminarycloud/project_ui_state/project_ui_state_pb2_grpc.pyi +44 -0
  29. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.py +48 -26
  30. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.pyi +30 -2
  31. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2_grpc.py +36 -0
  32. luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2_grpc.pyi +18 -0
  33. luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.py +153 -133
  34. luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.pyi +51 -3
  35. luminarycloud/_proto/client/simulation_pb2.py +261 -251
  36. luminarycloud/_proto/client/simulation_pb2.pyi +35 -2
  37. luminarycloud/_proto/frontend/output/output_pb2.py +24 -24
  38. luminarycloud/_proto/frontend/output/output_pb2.pyi +6 -3
  39. luminarycloud/_proto/geometry/geometry_pb2.py +63 -63
  40. luminarycloud/_proto/geometry/geometry_pb2.pyi +16 -8
  41. luminarycloud/_proto/hexmesh/hexmesh_pb2.py +17 -4
  42. luminarycloud/_proto/hexmesh/hexmesh_pb2.pyi +22 -1
  43. luminarycloud/_proto/inferenceservice/inferenceservice_pb2.py +10 -10
  44. luminarycloud/_proto/inferenceservice/inferenceservice_pb2.pyi +12 -7
  45. luminarycloud/_proto/quantity/quantity_pb2.py +19 -19
  46. luminarycloud/enum/geometry_status.py +15 -8
  47. luminarycloud/enum/pipeline_job_status.py +23 -0
  48. luminarycloud/feature_modification.py +3 -6
  49. luminarycloud/geometry.py +25 -0
  50. luminarycloud/geometry_version.py +23 -0
  51. luminarycloud/mesh.py +16 -0
  52. luminarycloud/params/enum/_enum_wrappers.py +29 -0
  53. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/fan_curve_inlet_.py +1 -1
  54. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/mach_inlet_.py +5 -1
  55. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/mass_flow_inlet_.py +5 -1
  56. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/total_pressure_inlet_.py +5 -1
  57. luminarycloud/params/simulation/physics/fluid/boundary_conditions/inlet/velocity_magnitude_inlet_.py +5 -1
  58. luminarycloud/physics_ai/inference.py +46 -30
  59. luminarycloud/pipelines/__init__.py +7 -0
  60. luminarycloud/pipelines/api.py +213 -0
  61. luminarycloud/project.py +98 -11
  62. luminarycloud/simulation_template.py +15 -6
  63. luminarycloud/vis/__init__.py +6 -0
  64. luminarycloud/vis/data_extraction.py +201 -31
  65. luminarycloud/vis/filters.py +94 -35
  66. luminarycloud/vis/interactive_inference.py +153 -0
  67. luminarycloud/vis/interactive_scene.py +35 -16
  68. luminarycloud/vis/primitives.py +87 -1
  69. luminarycloud/vis/visualization.py +50 -6
  70. luminarycloud/volume_selection.py +3 -6
  71. {luminarycloud-0.16.2.dist-info → luminarycloud-0.18.0.dist-info}/METADATA +18 -18
  72. {luminarycloud-0.16.2.dist-info → luminarycloud-0.18.0.dist-info}/RECORD +73 -62
  73. {luminarycloud-0.16.2.dist-info → luminarycloud-0.18.0.dist-info}/WHEEL +0 -0
@@ -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
- no_browser : bool
42
- (Optional) If True, prevents client from opening browser and raises an error if user
43
- interaction via the browser becomes required.
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
- no_browser: bool = False,
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.no_browser = no_browser # True to prevent client from opening browser
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.no_browser is True.
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("Attempting silent auth.")
230
- # https://auth0.com/docs/authenticate/login/configure-silent-authentication
231
- # TODO: This always fails due to lack of cookies: https://community.auth0.com/t/failed-silent-auth-login-required/33165/25?page=2
232
- response = requests.get(authorize_url + "&prompt=none", timeout=TIMEOUT)
233
- try:
234
- response.raise_for_status() # raise an error if we get a 40X status code
235
- callback_args = listener.block_until_callback()
236
- except KeyboardInterrupt:
237
- raise
238
- except Exception as e:
239
- logger.info("Silent auth failed.", exc_info=e)
240
- callback_args = MultiDict()
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"])
@@ -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, grpc_channel_options, channel_credentials, api_key
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
- if scaling is None:
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
- response = client.GetMesh(GetMeshRequest(id=mesh.id))
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
@@ -403,7 +403,6 @@ def params_to_dict(sim_params: SimulationParam) -> dict[str, Any]:
403
403
  tree = MessageToDict(
404
404
  sim_params,
405
405
  preserving_proto_field_name=True,
406
- including_default_value_fields=False,
407
406
  )
408
407
  helper = CondHelper()
409
408
  return helper.prune(tree)