luminarycloud 0.22.0__py3-none-any.whl → 0.22.1__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/_client/authentication_plugin.py +49 -0
- luminarycloud/_client/client.py +33 -8
- luminarycloud/_client/http_client.py +1 -1
- luminarycloud/_client/retry_interceptor.py +64 -2
- luminarycloud/_helpers/download.py +11 -0
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +132 -132
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +36 -8
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.py +53 -23
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.pyi +54 -1
- luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2.py +195 -0
- luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2.pyi +361 -0
- luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2_grpc.py +172 -0
- luminarycloud/_proto/api/v0/luminarycloud/physicsaiinference/physicsaiinference_pb2_grpc.pyi +66 -0
- luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.py +88 -65
- luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2.pyi +42 -0
- luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/thirdpartyintegration/onshape/onshape_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/base/base_pb2.py +7 -6
- luminarycloud/_proto/base/base_pb2.pyi +4 -0
- luminarycloud/_proto/client/simulation_pb2.py +3 -3
- luminarycloud/_proto/physicsaiinferenceservice/physicsaiinferenceservice_pb2.py +30 -0
- luminarycloud/_proto/physicsaiinferenceservice/physicsaiinferenceservice_pb2.pyi +7 -0
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2.py +2 -2
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.py +34 -0
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.pyi +12 -0
- luminarycloud/enum/vis_enums.py +6 -0
- luminarycloud/geometry.py +4 -0
- luminarycloud/geometry_version.py +4 -0
- luminarycloud/mesh.py +4 -0
- luminarycloud/meshing/mesh_generation_params.py +5 -6
- luminarycloud/meshing/sizing_strategy/sizing_strategies.py +1 -2
- luminarycloud/physics_ai/solution.py +4 -0
- luminarycloud/pipelines/api.py +99 -8
- luminarycloud/pipelines/core.py +1 -1
- luminarycloud/pipelines/stages.py +22 -9
- luminarycloud/project.py +5 -6
- luminarycloud/types/vector3.py +1 -2
- luminarycloud/vis/data_extraction.py +7 -7
- luminarycloud/vis/interactive_report.py +163 -7
- luminarycloud/vis/report.py +113 -1
- luminarycloud/volume_selection.py +10 -2
- {luminarycloud-0.22.0.dist-info → luminarycloud-0.22.1.dist-info}/METADATA +1 -1
- {luminarycloud-0.22.0.dist-info → luminarycloud-0.22.1.dist-info}/RECORD +44 -39
- {luminarycloud-0.22.0.dist-info → luminarycloud-0.22.1.dist-info}/WHEEL +1 -1
- luminarycloud/pipeline_util/dictable.py +0 -27
|
@@ -38,3 +38,52 @@ class AuthenticationPlugin(grpc.AuthMetadataPlugin):
|
|
|
38
38
|
callback(metadata, None)
|
|
39
39
|
except Exception as err:
|
|
40
40
|
callback(None, err)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AuthInterceptor(
|
|
44
|
+
grpc.UnaryUnaryClientInterceptor,
|
|
45
|
+
grpc.UnaryStreamClientInterceptor,
|
|
46
|
+
grpc.StreamUnaryClientInterceptor,
|
|
47
|
+
grpc.StreamStreamClientInterceptor,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
I need this as a workaround for container-to-host connections because I need to create a channel
|
|
51
|
+
that uses CallCredentials but doesn't use any ChannelCredentials. I.e. I need to authenticate
|
|
52
|
+
the requests, but I need the connection to be unencrypted. This is because the grpc server on
|
|
53
|
+
the native host isn't using SSL, so I can't use grpc.ssl_channel_credentials(), but it's also
|
|
54
|
+
not reachable on a loopback interface, so I can't use grpc.local_channel_credentials() either.
|
|
55
|
+
So I need to use a grpc.insecure_channel(), but you can't use CallCredentials with an insecure
|
|
56
|
+
channel. So the workaround is to use an interceptor instead of CallCredentials.
|
|
57
|
+
|
|
58
|
+
Also, I don't care about auth0, so I'm only supporting an API key.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, api_key: str):
|
|
62
|
+
self._api_key = api_key
|
|
63
|
+
|
|
64
|
+
def _augment(self, metadata):
|
|
65
|
+
return metadata + [("x-api-key", self._api_key)]
|
|
66
|
+
|
|
67
|
+
def intercept_unary_unary(self, continuation, client_call_details, request):
|
|
68
|
+
new_details = client_call_details._replace(
|
|
69
|
+
metadata=self._augment(client_call_details.metadata or [])
|
|
70
|
+
)
|
|
71
|
+
return continuation(new_details, request)
|
|
72
|
+
|
|
73
|
+
def intercept_unary_stream(self, continuation, client_call_details, request):
|
|
74
|
+
new_details = client_call_details._replace(
|
|
75
|
+
metadata=self._augment(client_call_details.metadata or [])
|
|
76
|
+
)
|
|
77
|
+
return continuation(new_details, request)
|
|
78
|
+
|
|
79
|
+
def intercept_stream_unary(self, continuation, client_call_details, request_iter):
|
|
80
|
+
new_details = client_call_details._replace(
|
|
81
|
+
metadata=self._augment(client_call_details.metadata or [])
|
|
82
|
+
)
|
|
83
|
+
return continuation(new_details, request_iter)
|
|
84
|
+
|
|
85
|
+
def intercept_stream_stream(self, continuation, client_call_details, request_iter):
|
|
86
|
+
new_details = client_call_details._replace(
|
|
87
|
+
metadata=self._augment(client_call_details.metadata or [])
|
|
88
|
+
)
|
|
89
|
+
return continuation(new_details, request_iter)
|
luminarycloud/_client/client.py
CHANGED
|
@@ -47,7 +47,7 @@ from .._proto.api.v0.luminarycloud.vis.vis_pb2_grpc import VisAPIServiceStub
|
|
|
47
47
|
from .._proto.api.v0.luminarycloud.feature_flag.feature_flag_pb2_grpc import (
|
|
48
48
|
FeatureFlagServiceStub,
|
|
49
49
|
)
|
|
50
|
-
from .authentication_plugin import AuthenticationPlugin
|
|
50
|
+
from .authentication_plugin import AuthenticationPlugin, AuthInterceptor
|
|
51
51
|
from .config import LC_DOMAIN, LC_API_KEY
|
|
52
52
|
from .logging_interceptor import LoggingInterceptor
|
|
53
53
|
from .retry_interceptor import RetryInterceptor
|
|
@@ -93,11 +93,20 @@ class Client(
|
|
|
93
93
|
The URL of the HTTP REST server. If not provided, it will default to the `target`.
|
|
94
94
|
localhost : bool
|
|
95
95
|
True if the API server is running locally.
|
|
96
|
+
insecure_grpc_channel : bool
|
|
97
|
+
True to use an unencrypted gRPC channel, even though requests are authenticated. There's no
|
|
98
|
+
legitimate reason to do this outside of a local development situation where the SDK is
|
|
99
|
+
running from a container and connecting to an API server that is running on the host.
|
|
96
100
|
grpc_channel_options : Optional[Iterable[tuple[str, str]]]
|
|
97
101
|
A list of gRPC channel args. The full list is available here:
|
|
98
102
|
https://github.com/grpc/grpc/blob/v1.46.x/include/grpc/impl/codegen/grpc_types.h
|
|
99
103
|
api_key : Optional[str]
|
|
100
104
|
The API key to use for authentication.
|
|
105
|
+
log_retries : bool
|
|
106
|
+
True to log each retriable error response. There are some errors the API server may return
|
|
107
|
+
that are known to be transient, and the client will always retry requests when it gets one
|
|
108
|
+
of them. By default, the client retries silently. Set this to True to log the error
|
|
109
|
+
responses (at INFO level) for the retriable errors.
|
|
101
110
|
**kwargs : dict, optional
|
|
102
111
|
Additional arguments are passed to Auth0Client. See _auth/auth.py.
|
|
103
112
|
|
|
@@ -116,9 +125,11 @@ class Client(
|
|
|
116
125
|
target: str = LC_DOMAIN,
|
|
117
126
|
http_target: str | None = None,
|
|
118
127
|
localhost: bool = False,
|
|
128
|
+
insecure_grpc_channel: bool = False,
|
|
119
129
|
grpc_channel_options: Optional[Iterable[tuple[str, Union[str, int]]]] = None,
|
|
120
130
|
channel_credentials: Optional[grpc.ChannelCredentials] = None,
|
|
121
131
|
api_key: Optional[str] = LC_API_KEY,
|
|
132
|
+
log_retries: bool = False,
|
|
122
133
|
**kwargs: Any,
|
|
123
134
|
):
|
|
124
135
|
self._target = target
|
|
@@ -139,7 +150,12 @@ class Client(
|
|
|
139
150
|
if grpc_channel_options:
|
|
140
151
|
grpc_channel_options_with_keep_alive.extend(grpc_channel_options)
|
|
141
152
|
self._channel = self._create_channel(
|
|
142
|
-
localhost,
|
|
153
|
+
localhost,
|
|
154
|
+
insecure_grpc_channel,
|
|
155
|
+
grpc_channel_options_with_keep_alive,
|
|
156
|
+
channel_credentials,
|
|
157
|
+
api_key,
|
|
158
|
+
log_retries,
|
|
143
159
|
)
|
|
144
160
|
self._context_tokens: list[Token] = []
|
|
145
161
|
self.__register_rpcs()
|
|
@@ -193,9 +209,11 @@ class Client(
|
|
|
193
209
|
def _create_channel(
|
|
194
210
|
self,
|
|
195
211
|
localhost: bool = False,
|
|
212
|
+
insecure: bool = False,
|
|
196
213
|
grpc_channel_options: Optional[Iterable[tuple[str, Union[str, int]]]] = None,
|
|
197
214
|
channel_credentials: Optional[grpc.ChannelCredentials] = None,
|
|
198
215
|
api_key: Optional[str] = None,
|
|
216
|
+
log_retries: bool = False,
|
|
199
217
|
) -> grpc.Channel:
|
|
200
218
|
if channel_credentials is None:
|
|
201
219
|
if localhost:
|
|
@@ -217,15 +235,22 @@ class Client(
|
|
|
217
235
|
call_creds = grpc.metadata_call_credentials(auth_plugin)
|
|
218
236
|
composite_creds = grpc.composite_channel_credentials(channel_credentials, call_creds)
|
|
219
237
|
options = grpc_channel_options and list(grpc_channel_options)
|
|
220
|
-
|
|
221
|
-
self._target,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
if insecure:
|
|
239
|
+
channel = grpc.insecure_channel(self._target, options=options)
|
|
240
|
+
channel = grpc.intercept_channel(
|
|
241
|
+
channel,
|
|
242
|
+
AuthInterceptor(api_key),
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
channel = grpc.secure_channel(
|
|
246
|
+
self._target,
|
|
247
|
+
composite_creds,
|
|
248
|
+
options=options,
|
|
249
|
+
)
|
|
225
250
|
intercepted_channel = grpc.intercept_channel(
|
|
226
251
|
channel,
|
|
227
252
|
LoggingInterceptor(),
|
|
228
|
-
RetryInterceptor(),
|
|
253
|
+
RetryInterceptor(log_retries),
|
|
229
254
|
)
|
|
230
255
|
return add_instrumentation(
|
|
231
256
|
intercepted_channel,
|
|
@@ -32,7 +32,7 @@ class HttpClient:
|
|
|
32
32
|
api_key: str | None = None,
|
|
33
33
|
auth0_client: Auth0Client | None = None,
|
|
34
34
|
*,
|
|
35
|
-
timeout: int =
|
|
35
|
+
timeout: int | None = 300,
|
|
36
36
|
retries: int = 3,
|
|
37
37
|
backoff_factor: float = 0.3,
|
|
38
38
|
retriable_status_codes: tuple = (500, 502, 503, 504, 429),
|
|
@@ -2,19 +2,68 @@
|
|
|
2
2
|
from collections.abc import Callable
|
|
3
3
|
from time import sleep
|
|
4
4
|
from typing import Any
|
|
5
|
+
import logging
|
|
5
6
|
|
|
6
7
|
import grpc
|
|
7
8
|
from grpc import (
|
|
8
9
|
ClientCallDetails,
|
|
9
10
|
UnaryUnaryClientInterceptor,
|
|
10
11
|
)
|
|
12
|
+
from grpc_status.rpc_status import GRPC_DETAILS_METADATA_KEY
|
|
11
13
|
|
|
12
14
|
from luminarycloud.exceptions import AuthenticationError
|
|
15
|
+
from luminarycloud._proto.base import base_pb2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_rate_limited(call: grpc.Call) -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Check if a gRPC call failed due to rate limiting.
|
|
21
|
+
|
|
22
|
+
Rate limit errors are identified with the SUBCODE_RATE_LIMITED subcode and UNAVAILABLE status.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
call: The gRPC call to check
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
True if the error is a rate limit error, False otherwise
|
|
29
|
+
"""
|
|
30
|
+
if call.code() != grpc.StatusCode.UNAVAILABLE:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
# Get the trailing metadata which contains error details
|
|
35
|
+
# Metadata is a sequence of tuples (key, value)
|
|
36
|
+
for key, value in call.trailing_metadata() or []:
|
|
37
|
+
if key != GRPC_DETAILS_METADATA_KEY or not isinstance(value, bytes):
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
status = base_pb2.Status()
|
|
41
|
+
status.ParseFromString(value)
|
|
42
|
+
for any_detail in status.details:
|
|
43
|
+
if any_detail.Is(base_pb2.StatusPayload.DESCRIPTOR):
|
|
44
|
+
payload = base_pb2.StatusPayload()
|
|
45
|
+
any_detail.Unpack(payload)
|
|
46
|
+
if payload.subcode == base_pb2.SUBCODE_RATE_LIMITED:
|
|
47
|
+
return True
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
13
54
|
|
|
14
55
|
|
|
15
56
|
class RetryInterceptor(UnaryUnaryClientInterceptor):
|
|
57
|
+
def __init__(self, log_retries: bool = False):
|
|
58
|
+
self.log_retries = log_retries
|
|
59
|
+
super().__init__()
|
|
60
|
+
|
|
16
61
|
"""
|
|
17
|
-
A retry interceptor that retries on
|
|
62
|
+
A retry interceptor that retries on rate limit errors and other retryable errors.
|
|
63
|
+
|
|
64
|
+
This interceptor handles:
|
|
65
|
+
1. Rate limit errors (UNAVAILABLE with SUBCODE_RATE_LIMITED) - always retried
|
|
66
|
+
2. [grpc.StatusCode.RESOURCE_EXHAUSTED, grpc.StatusCode.UNAVAILABLE] - retried
|
|
18
67
|
|
|
19
68
|
This is required because, while the retry policy for the gRPC client is configurable via
|
|
20
69
|
https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto,
|
|
@@ -43,7 +92,10 @@ class RetryInterceptor(UnaryUnaryClientInterceptor):
|
|
|
43
92
|
n_max_retries = 20
|
|
44
93
|
max_retry_seconds = 20
|
|
45
94
|
backoffs = [min(i * 2, max_retry_seconds) for i in range(1, n_max_retries)]
|
|
46
|
-
|
|
95
|
+
backoff_index = 0
|
|
96
|
+
while True:
|
|
97
|
+
if backoff_index >= len(backoffs):
|
|
98
|
+
break
|
|
47
99
|
call = continuation(client_call_details, request)
|
|
48
100
|
if call.code() not in retryable_codes:
|
|
49
101
|
break
|
|
@@ -54,7 +106,17 @@ class RetryInterceptor(UnaryUnaryClientInterceptor):
|
|
|
54
106
|
details = call.details() or ""
|
|
55
107
|
if "InteractiveAuthException" in details:
|
|
56
108
|
break
|
|
109
|
+
backoff = backoffs[backoff_index]
|
|
110
|
+
if self.log_retries:
|
|
111
|
+
logger.info(
|
|
112
|
+
f"Retrying {client_call_details.method} in {backoff} seconds (last response: {call.code()}, {call.details()})"
|
|
113
|
+
)
|
|
57
114
|
sleep(backoff)
|
|
115
|
+
# Keep retrying rate-limited calls while increasing the backoff up to the max.
|
|
116
|
+
backoff_index += 1
|
|
117
|
+
if _is_rate_limited(call):
|
|
118
|
+
backoff_index = max(min(backoff_index, len(backoffs) - 2), 0)
|
|
119
|
+
|
|
58
120
|
try:
|
|
59
121
|
call.result()
|
|
60
122
|
except grpc.RpcError as e:
|
|
@@ -17,6 +17,7 @@ from .._proto.api.v0.luminarycloud.solution.solution_pb2 import (
|
|
|
17
17
|
)
|
|
18
18
|
from .._proto.api.v0.luminarycloud.physics_ai.physics_ai_pb2 import (
|
|
19
19
|
GetSolutionDataPhysicsAIRequest,
|
|
20
|
+
SurfaceGroup,
|
|
20
21
|
)
|
|
21
22
|
from .._client import Client
|
|
22
23
|
from ..enum.quantity_type import QuantityType
|
|
@@ -140,6 +141,7 @@ def download_solution_physics_ai(
|
|
|
140
141
|
process_volume: bool = False,
|
|
141
142
|
single_precision: bool = False,
|
|
142
143
|
internal_options: Optional[Dict[str, str]] = None,
|
|
144
|
+
export_surface_groups: Optional[Dict[str, List[str]]] = None,
|
|
143
145
|
) -> Optional[FileChunkStream]:
|
|
144
146
|
"""
|
|
145
147
|
Returns the download as a file-like object, or None if destination_url is provided.
|
|
@@ -165,6 +167,9 @@ def download_solution_physics_ai(
|
|
|
165
167
|
Whether to process volume meshes during physics AI processing.
|
|
166
168
|
single_precision: bool
|
|
167
169
|
If True, the solution will be downloaded in single precision.
|
|
170
|
+
export_surface_groups: Optional[Dict[str, List[str]]]
|
|
171
|
+
Dictionary mapping group names to lists of surface names.
|
|
172
|
+
Each group will be exported as an individual STL file.
|
|
168
173
|
|
|
169
174
|
Examples
|
|
170
175
|
--------
|
|
@@ -173,6 +178,11 @@ def download_solution_physics_ai(
|
|
|
173
178
|
... fp.write(dl.read())
|
|
174
179
|
"""
|
|
175
180
|
|
|
181
|
+
surface_groups_pb = []
|
|
182
|
+
if export_surface_groups:
|
|
183
|
+
for group_name, surfaces in export_surface_groups.items():
|
|
184
|
+
surface_groups_pb.append(SurfaceGroup(name=group_name, surfaces=surfaces))
|
|
185
|
+
|
|
176
186
|
request = GetSolutionDataPhysicsAIRequest(
|
|
177
187
|
solution_id=solution_id,
|
|
178
188
|
exclude_surfaces=exclude_surfaces or [],
|
|
@@ -186,6 +196,7 @@ def download_solution_physics_ai(
|
|
|
186
196
|
process_volume=process_volume,
|
|
187
197
|
single_precision=single_precision,
|
|
188
198
|
internal_options=internal_options or {},
|
|
199
|
+
export_surface_groups=surface_groups_pb,
|
|
189
200
|
)
|
|
190
201
|
response = client.GetSolutionDataPhysicsAI(request)
|
|
191
202
|
|