luminarycloud 0.19.0__py3-none-any.whl → 0.22.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/__init__.py +5 -1
- luminarycloud/_client/client.py +7 -0
- luminarycloud/_client/http_client.py +10 -8
- luminarycloud/_feature_flag.py +22 -0
- luminarycloud/_helpers/_create_simulation.py +7 -2
- luminarycloud/_helpers/_upload_mesh.py +1 -0
- luminarycloud/_helpers/_wait_for_mesh.py +6 -5
- luminarycloud/_helpers/_wait_for_simulation.py +3 -3
- luminarycloud/_helpers/download.py +3 -1
- luminarycloud/_helpers/pagination.py +62 -0
- luminarycloud/_helpers/proto_decorator.py +13 -5
- luminarycloud/_helpers/upload.py +18 -12
- luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2.py +55 -0
- luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2.pyi +52 -0
- luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2_grpc.py +72 -0
- luminarycloud/_proto/api/v0/luminarycloud/feature_flag/feature_flag_pb2_grpc.pyi +35 -0
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.py +168 -124
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2.pyi +133 -4
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.py +66 -0
- luminarycloud/_proto/api/v0/luminarycloud/geometry/geometry_pb2_grpc.pyi +20 -0
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.py +8 -8
- luminarycloud/_proto/api/v0/luminarycloud/inference/inference_pb2.pyi +5 -5
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.py +74 -73
- luminarycloud/_proto/api/v0/luminarycloud/mesh/mesh_pb2.pyi +17 -3
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.py +96 -25
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2.pyi +235 -1
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/physics_ai/physics_ai_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.py +16 -16
- luminarycloud/_proto/api/v0/luminarycloud/project/project_pb2.pyi +7 -3
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.py +97 -61
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2.pyi +77 -4
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.py +34 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation/simulation_pb2_grpc.pyi +12 -0
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.py +33 -31
- luminarycloud/_proto/api/v0/luminarycloud/simulation_template/simulation_template_pb2.pyi +23 -2
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.py +126 -27
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2.pyi +183 -0
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2_grpc.py +99 -0
- luminarycloud/_proto/api/v0/luminarycloud/vis/vis_pb2_grpc.pyi +30 -0
- luminarycloud/_proto/assistant/assistant_pb2.py +74 -41
- luminarycloud/_proto/assistant/assistant_pb2.pyi +64 -2
- luminarycloud/_proto/assistant/assistant_pb2_grpc.py +33 -0
- luminarycloud/_proto/assistant/assistant_pb2_grpc.pyi +10 -0
- luminarycloud/_proto/base/base_pb2.py +20 -7
- luminarycloud/_proto/base/base_pb2.pyi +38 -0
- luminarycloud/_proto/cad/shape_pb2.py +39 -19
- luminarycloud/_proto/cad/shape_pb2.pyi +86 -34
- luminarycloud/_proto/cad/transformation_pb2.py +60 -16
- luminarycloud/_proto/cad/transformation_pb2.pyi +138 -32
- luminarycloud/_proto/client/simulation_pb2.py +501 -348
- luminarycloud/_proto/client/simulation_pb2.pyi +607 -11
- luminarycloud/_proto/geometry/geometry_pb2.py +77 -63
- luminarycloud/_proto/geometry/geometry_pb2.pyi +42 -3
- luminarycloud/_proto/hexmesh/hexmesh_pb2.py +24 -18
- luminarycloud/_proto/hexmesh/hexmesh_pb2.pyi +23 -2
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.py +10 -10
- luminarycloud/_proto/inferenceservice/inferenceservice_pb2.pyi +5 -5
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2.py +29 -0
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2.pyi +7 -0
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.py +70 -0
- luminarycloud/_proto/physicsaitrainingservice/physicsaitrainingservice_pb2_grpc.pyi +30 -0
- luminarycloud/_proto/quantity/quantity_options_pb2.py +6 -6
- luminarycloud/_proto/quantity/quantity_options_pb2.pyi +10 -1
- luminarycloud/_proto/quantity/quantity_pb2.py +176 -167
- luminarycloud/_proto/quantity/quantity_pb2.pyi +11 -5
- luminarycloud/enum/__init__.py +1 -0
- luminarycloud/enum/gpu_type.py +2 -0
- luminarycloud/enum/quantity_type.py +9 -0
- luminarycloud/enum/vis_enums.py +23 -3
- luminarycloud/exceptions.py +7 -1
- luminarycloud/feature_modification.py +45 -35
- luminarycloud/geometry.py +107 -9
- luminarycloud/geometry_version.py +57 -3
- luminarycloud/mesh.py +1 -2
- luminarycloud/meshing/mesh_generation_params.py +8 -8
- luminarycloud/params/enum/_enum_wrappers.py +562 -30
- luminarycloud/params/simulation/adaptive_mesh_refinement_.py +4 -0
- luminarycloud/params/simulation/material/material_solid_.py +15 -1
- luminarycloud/params/simulation/physics/__init__.py +0 -1
- luminarycloud/params/simulation/physics/periodic_pair_.py +12 -31
- luminarycloud/physics_ai/architectures.py +58 -0
- luminarycloud/physics_ai/inference.py +13 -13
- luminarycloud/physics_ai/solution.py +3 -1
- luminarycloud/physics_ai/training_jobs.py +37 -0
- luminarycloud/pipelines/__init__.py +11 -3
- luminarycloud/pipelines/api.py +248 -16
- luminarycloud/pipelines/arguments.py +15 -0
- luminarycloud/pipelines/core.py +113 -96
- luminarycloud/pipelines/{operators.py → stages.py} +96 -39
- luminarycloud/project.py +15 -47
- luminarycloud/simulation.py +69 -5
- luminarycloud/simulation_param.py +0 -9
- luminarycloud/simulation_template.py +2 -1
- luminarycloud/types/matrix3.py +12 -0
- luminarycloud/vis/__init__.py +17 -0
- luminarycloud/vis/data_extraction.py +20 -4
- luminarycloud/vis/interactive_report.py +110 -0
- luminarycloud/vis/interactive_scene.py +29 -2
- luminarycloud/vis/report.py +252 -0
- luminarycloud/vis/visualization.py +127 -5
- luminarycloud/volume_selection.py +132 -69
- {luminarycloud-0.19.0.dist-info → luminarycloud-0.22.0.dist-info}/METADATA +1 -1
- {luminarycloud-0.19.0.dist-info → luminarycloud-0.22.0.dist-info}/RECORD +105 -97
- luminarycloud/params/simulation/physics/periodic_pair/__init__.py +0 -2
- luminarycloud/params/simulation/physics/periodic_pair/periodicity_type/__init__.py +0 -2
- luminarycloud/params/simulation/physics/periodic_pair/periodicity_type/rotational_periodicity_.py +0 -31
- luminarycloud/params/simulation/physics/periodic_pair/periodicity_type/translational_periodicity_.py +0 -29
- luminarycloud/params/simulation/physics/periodic_pair/periodicity_type_.py +0 -25
- {luminarycloud-0.19.0.dist-info → luminarycloud-0.22.0.dist-info}/WHEEL +0 -0
luminarycloud/__init__.py
CHANGED
|
@@ -13,18 +13,22 @@ from . import (
|
|
|
13
13
|
types as types,
|
|
14
14
|
vis as vis,
|
|
15
15
|
)
|
|
16
|
-
|
|
17
16
|
from ._client import (
|
|
18
17
|
Client as Client,
|
|
19
18
|
get_default_client as get_default_client,
|
|
20
19
|
set_default_client as set_default_client,
|
|
21
20
|
)
|
|
21
|
+
from ._feature_flag import (
|
|
22
|
+
_get_feature_flags as _get_feature_flags,
|
|
23
|
+
)
|
|
22
24
|
from .geometry import (
|
|
23
25
|
get_geometry as get_geometry,
|
|
24
26
|
Geometry as Geometry,
|
|
25
27
|
)
|
|
26
28
|
from .geometry_version import (
|
|
27
29
|
GeometryVersion as GeometryVersion,
|
|
30
|
+
get_geometry_version as get_geometry_version,
|
|
31
|
+
update_geometry_version as update_geometry_version,
|
|
28
32
|
)
|
|
29
33
|
from .mesh import (
|
|
30
34
|
get_mesh as get_mesh,
|
luminarycloud/_client/client.py
CHANGED
|
@@ -44,6 +44,9 @@ from .._proto.api.v0.luminarycloud.project_ui_state.project_ui_state_pb2_grpc im
|
|
|
44
44
|
from .._proto.api.v0.luminarycloud.solution.solution_pb2_grpc import SolutionServiceStub
|
|
45
45
|
from .._proto.api.v0.luminarycloud.upload.upload_pb2_grpc import UploadServiceStub
|
|
46
46
|
from .._proto.api.v0.luminarycloud.vis.vis_pb2_grpc import VisAPIServiceStub
|
|
47
|
+
from .._proto.api.v0.luminarycloud.feature_flag.feature_flag_pb2_grpc import (
|
|
48
|
+
FeatureFlagServiceStub,
|
|
49
|
+
)
|
|
47
50
|
from .authentication_plugin import AuthenticationPlugin
|
|
48
51
|
from .config import LC_DOMAIN, LC_API_KEY
|
|
49
52
|
from .logging_interceptor import LoggingInterceptor
|
|
@@ -71,6 +74,7 @@ class Client(
|
|
|
71
74
|
InferenceServiceStub,
|
|
72
75
|
OnshapeServiceStub,
|
|
73
76
|
ProjectUIStateServiceStub,
|
|
77
|
+
FeatureFlagServiceStub,
|
|
74
78
|
):
|
|
75
79
|
"""
|
|
76
80
|
Creates a Luminary API client.
|
|
@@ -129,6 +133,8 @@ class Client(
|
|
|
129
133
|
("grpc.keepalive_timeout_ms", 5000),
|
|
130
134
|
("grpc.keepalive_permit_without_calls", 1),
|
|
131
135
|
("grpc.http2.max_pings_without_data", 10),
|
|
136
|
+
("grpc.max_receive_message_length", 32 * 1024 * 1024),
|
|
137
|
+
("grpc.max_send_message_length", 32 * 1024 * 1024),
|
|
132
138
|
]
|
|
133
139
|
if grpc_channel_options:
|
|
134
140
|
grpc_channel_options_with_keep_alive.extend(grpc_channel_options)
|
|
@@ -246,6 +252,7 @@ class Client(
|
|
|
246
252
|
NamedVariableSetServiceStub.__init__(self, self._channel)
|
|
247
253
|
OnshapeServiceStub.__init__(self, self._channel)
|
|
248
254
|
ProjectUIStateServiceStub.__init__(self, self._channel)
|
|
255
|
+
FeatureFlagServiceStub.__init__(self, self._channel)
|
|
249
256
|
for name, value in self.__dict__.items():
|
|
250
257
|
if isinstance(value, grpc.UnaryUnaryMultiCallable):
|
|
251
258
|
setattr(self, name, rpc_error(value))
|
|
@@ -113,9 +113,11 @@ class HttpClient:
|
|
|
113
113
|
f"Bearer {self.auth0_client.fetch_access_token()}"
|
|
114
114
|
)
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
# This method, which takes a full URL instead of just a path, is made public so the upload
|
|
117
|
+
# helper can use it to make authenticated requests to a gcsproxy endpoint which is hosted by
|
|
118
|
+
# jobmaster, not apiserver
|
|
119
|
+
def raw_request(self, method: str, url: str, **kwargs) -> requests.Response:
|
|
117
120
|
self._authenticate_session()
|
|
118
|
-
url = self._url(path)
|
|
119
121
|
self._log_request(method, url, kwargs)
|
|
120
122
|
resp = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
|
121
123
|
self._log_response(resp)
|
|
@@ -128,22 +130,22 @@ class HttpClient:
|
|
|
128
130
|
|
|
129
131
|
# ---- Raw methods ----
|
|
130
132
|
def raw_get(self, path: str, **kwargs) -> requests.Response:
|
|
131
|
-
return self.
|
|
133
|
+
return self.raw_request("GET", self._url(path), **kwargs)
|
|
132
134
|
|
|
133
135
|
def raw_post(self, path: str, body: dict | None = None, **kwargs) -> requests.Response:
|
|
134
|
-
return self.
|
|
136
|
+
return self.raw_request("POST", self._url(path), json=body, **kwargs)
|
|
135
137
|
|
|
136
138
|
def raw_put(self, path: str, body: dict | None = None, **kwargs) -> requests.Response:
|
|
137
|
-
return self.
|
|
139
|
+
return self.raw_request("PUT", self._url(path), json=body, **kwargs)
|
|
138
140
|
|
|
139
141
|
def raw_patch(self, path: str, body: dict | None = None, **kwargs) -> requests.Response:
|
|
140
|
-
return self.
|
|
142
|
+
return self.raw_request("PATCH", self._url(path), json=body, **kwargs)
|
|
141
143
|
|
|
142
144
|
def raw_delete(self, path: str, **kwargs) -> requests.Response:
|
|
143
|
-
return self.
|
|
145
|
+
return self.raw_request("DELETE", self._url(path), **kwargs)
|
|
144
146
|
|
|
145
147
|
def raw_head(self, path: str, **kwargs) -> requests.Response:
|
|
146
|
-
return self.
|
|
148
|
+
return self.raw_request("HEAD", self._url(path), **kwargs)
|
|
147
149
|
|
|
148
150
|
# ---- JSON convenience methods ----
|
|
149
151
|
def get(self, path: str, **kwargs) -> dict:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright 2023-2025 Luminary Cloud, Inc. All Rights Reserved.
|
|
2
|
+
from google.protobuf import empty_pb2
|
|
3
|
+
|
|
4
|
+
from ._proto.api.v0.luminarycloud.feature_flag import feature_flag_pb2
|
|
5
|
+
from ._client import get_default_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_feature_flags() -> dict[int, str]:
|
|
9
|
+
"""Get enabled feature flags for the authenticated user.
|
|
10
|
+
|
|
11
|
+
Feature flags are used to control access to experimental and internal features.
|
|
12
|
+
Each flag is identified by an experiment ID and a feature flag name.
|
|
13
|
+
|
|
14
|
+
Returns
|
|
15
|
+
-------
|
|
16
|
+
dict[int, str]
|
|
17
|
+
A dictionary mapping experiment IDs (uint64) to feature flag names.
|
|
18
|
+
Returns an empty dictionary if no feature flags are enabled for the user.
|
|
19
|
+
"""
|
|
20
|
+
req = empty_pb2.Empty()
|
|
21
|
+
res = get_default_client().GetFeatureFlags(req)
|
|
22
|
+
return dict(res.enabled_feature_flags)
|
|
@@ -7,6 +7,9 @@ from .._proto.api.v0.luminarycloud.simulation.simulation_pb2 import (
|
|
|
7
7
|
SimulationOptions,
|
|
8
8
|
CreateSimulationRequest,
|
|
9
9
|
)
|
|
10
|
+
from .._proto.api.v0.luminarycloud.simulation_template.simulation_template_pb2 import (
|
|
11
|
+
SimulationTemplate,
|
|
12
|
+
)
|
|
10
13
|
from .._client import Client
|
|
11
14
|
from ..enum import GPUType
|
|
12
15
|
|
|
@@ -18,8 +21,9 @@ def create_simulation(
|
|
|
18
21
|
project_id: str,
|
|
19
22
|
mesh_id: str,
|
|
20
23
|
name: str,
|
|
21
|
-
simulation_template_id: str,
|
|
24
|
+
simulation_template_id: Optional[str] = None,
|
|
22
25
|
*,
|
|
26
|
+
simulation_template: Optional[SimulationTemplate] = None,
|
|
23
27
|
named_variable_set_version_id: Optional[str] = None,
|
|
24
28
|
description: str = "",
|
|
25
29
|
batch_processing: bool = True,
|
|
@@ -32,7 +36,8 @@ def create_simulation(
|
|
|
32
36
|
CreateSimulationRequest(
|
|
33
37
|
project_id=project_id,
|
|
34
38
|
mesh_id=mesh_id,
|
|
35
|
-
simulation_template_id=simulation_template_id,
|
|
39
|
+
simulation_template_id=simulation_template_id or "",
|
|
40
|
+
simulation_template=simulation_template,
|
|
36
41
|
named_variable_set_version_id=named_variable_set_version_id or "",
|
|
37
42
|
name=name,
|
|
38
43
|
description=description,
|
|
@@ -137,6 +137,7 @@ def _upload_mesh_from_path(
|
|
|
137
137
|
logger.debug(f"started upload")
|
|
138
138
|
|
|
139
139
|
gcs_resumable_upload(
|
|
140
|
+
client=client,
|
|
140
141
|
filepath=pathlib.Path(filepath),
|
|
141
142
|
signed_url=start_res.upload.gcs_resumable.signed_url,
|
|
142
143
|
http_headers=start_res.upload.gcs_resumable.http_headers,
|
|
@@ -1,7 +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
|
-
from luminarycloud.exceptions import DeadlineExceededError
|
|
4
|
+
from luminarycloud.exceptions import DeadlineExceededError, Timeout
|
|
5
5
|
|
|
6
6
|
from .._proto.api.v0.luminarycloud.mesh.mesh_pb2 import Mesh, GetMeshRequest
|
|
7
7
|
from .._client import Client
|
|
@@ -32,19 +32,20 @@ def wait_for_mesh(
|
|
|
32
32
|
|
|
33
33
|
Raises
|
|
34
34
|
------
|
|
35
|
-
|
|
35
|
+
Timeout
|
|
36
36
|
"""
|
|
37
37
|
deadline = time() + timeout_seconds
|
|
38
38
|
while True:
|
|
39
39
|
if time() >= deadline:
|
|
40
|
-
|
|
41
|
-
raise TimeoutError
|
|
40
|
+
raise Timeout("Timed out waiting for mesh to be ready.")
|
|
42
41
|
# It seems this call sometimes hangs. It seems as well that python gRPC is known to hang
|
|
43
42
|
# in some cases, so we'll add a deadline and catch deadline exceeded errors to retry.
|
|
44
43
|
try:
|
|
45
44
|
response = client.GetMesh(GetMeshRequest(id=mesh.id), timeout=15)
|
|
46
45
|
except DeadlineExceededError:
|
|
47
|
-
logger.
|
|
46
|
+
logger.debug(
|
|
47
|
+
"Request deadline exceeded while waiting for mesh status. Will retry if the `wait_for_mesh` deadline has not expired."
|
|
48
|
+
)
|
|
48
49
|
sleep(max(0, min(interval_seconds, deadline - time())))
|
|
49
50
|
continue
|
|
50
51
|
status = response.mesh.status
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import io
|
|
3
3
|
import logging
|
|
4
4
|
from time import time, sleep
|
|
5
|
+
from luminarycloud.exceptions import Timeout
|
|
5
6
|
|
|
6
7
|
from .._proto.api.v0.luminarycloud.simulation.simulation_pb2 import (
|
|
7
8
|
GetSimulationResponse,
|
|
@@ -42,7 +43,7 @@ def wait_for_simulation(
|
|
|
42
43
|
|
|
43
44
|
Raises
|
|
44
45
|
------
|
|
45
|
-
|
|
46
|
+
Timeout
|
|
46
47
|
"""
|
|
47
48
|
deadline = time() + timeout_seconds
|
|
48
49
|
# latest_iter is used to avoid printing the same iteration's residuals multiple times
|
|
@@ -66,8 +67,7 @@ def wait_for_simulation(
|
|
|
66
67
|
]:
|
|
67
68
|
return status
|
|
68
69
|
if time() >= deadline:
|
|
69
|
-
|
|
70
|
-
raise TimeoutError
|
|
70
|
+
raise Timeout("Timed out waiting for simulation to finish")
|
|
71
71
|
sleep(max(0, min(interval_seconds, deadline - time())))
|
|
72
72
|
|
|
73
73
|
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import requests
|
|
7
|
-
from typing import Any, Iterator, Optional, Union, cast, List
|
|
7
|
+
from typing import Any, Iterator, Optional, Union, cast, List, Dict
|
|
8
8
|
|
|
9
9
|
from .file_chunk_stream import FileChunkStream
|
|
10
10
|
from .._proto.api.v0.luminarycloud.common import common_pb2 as commonpb
|
|
@@ -139,6 +139,7 @@ def download_solution_physics_ai(
|
|
|
139
139
|
volume_fields_to_keep: Optional[List[QuantityType]] = None,
|
|
140
140
|
process_volume: bool = False,
|
|
141
141
|
single_precision: bool = False,
|
|
142
|
+
internal_options: Optional[Dict[str, str]] = None,
|
|
142
143
|
) -> Optional[FileChunkStream]:
|
|
143
144
|
"""
|
|
144
145
|
Returns the download as a file-like object, or None if destination_url is provided.
|
|
@@ -184,6 +185,7 @@ def download_solution_physics_ai(
|
|
|
184
185
|
),
|
|
185
186
|
process_volume=process_volume,
|
|
186
187
|
single_precision=single_precision,
|
|
188
|
+
internal_options=internal_options or {},
|
|
187
189
|
)
|
|
188
190
|
response = client.GetSolutionDataPhysicsAI(request)
|
|
189
191
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Generic, Optional, TypeVar
|
|
3
|
+
|
|
4
|
+
from .._client import get_default_client
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PaginationIterator(ABC, Generic[T]):
|
|
10
|
+
"""Generic iterator class that provides length hint for paginated results."""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def _fetch_page(self, page_size: int, page_token: str) -> tuple[list[T], str, int]:
|
|
14
|
+
"""
|
|
15
|
+
Fetch a page of results from the server. Must return a tuple of the list of results, the
|
|
16
|
+
next page token, and the total count of results.
|
|
17
|
+
"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def __init__(self, page_size: int):
|
|
21
|
+
self._page_size: int = page_size
|
|
22
|
+
self._page_token: str = ""
|
|
23
|
+
self._total_count: Optional[int] = None
|
|
24
|
+
self._current_page: Optional[list[T]] = None
|
|
25
|
+
self._client = get_default_client()
|
|
26
|
+
self._iterated_count: int = 0
|
|
27
|
+
|
|
28
|
+
def __iter__(self) -> "PaginationIterator[T]":
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def __next__(self) -> T:
|
|
32
|
+
if self._current_page is None:
|
|
33
|
+
self._fetch_next_page()
|
|
34
|
+
|
|
35
|
+
# _current_page really can't be None here, but this assertion is needed to appease mypy
|
|
36
|
+
assert self._current_page is not None
|
|
37
|
+
|
|
38
|
+
if len(self._current_page) == 0:
|
|
39
|
+
if not self._page_token:
|
|
40
|
+
raise StopIteration
|
|
41
|
+
self._fetch_next_page()
|
|
42
|
+
|
|
43
|
+
self._iterated_count += 1
|
|
44
|
+
|
|
45
|
+
return self._current_page.pop(0)
|
|
46
|
+
|
|
47
|
+
def _fetch_next_page(self) -> None:
|
|
48
|
+
items, next_page_token, total_count = self._fetch_page(self._page_size, self._page_token)
|
|
49
|
+
|
|
50
|
+
self._current_page = items
|
|
51
|
+
self._page_token = next_page_token
|
|
52
|
+
|
|
53
|
+
# Set length hint on first fetch if available
|
|
54
|
+
if self._total_count is None:
|
|
55
|
+
self._total_count = total_count
|
|
56
|
+
|
|
57
|
+
def __length_hint__(self) -> int:
|
|
58
|
+
if self._total_count is None:
|
|
59
|
+
# Fetch first page to get total size if not already fetched
|
|
60
|
+
if self._current_page is None:
|
|
61
|
+
self._fetch_next_page()
|
|
62
|
+
return max(0, (self._total_count or 0) - self._iterated_count)
|
|
@@ -9,6 +9,9 @@ from typing import (
|
|
|
9
9
|
from google.protobuf.message import Message
|
|
10
10
|
|
|
11
11
|
from luminarycloud.types import Vector3
|
|
12
|
+
from luminarycloud.types.adfloat import _to_ad_proto
|
|
13
|
+
from luminarycloud.types.vector3 import _to_vector3_ad_proto
|
|
14
|
+
from .._proto.base.base_pb2 import AdFloatType, AdVector3
|
|
12
15
|
|
|
13
16
|
P = TypeVar("P", bound=Message)
|
|
14
17
|
C = TypeVar("C")
|
|
@@ -36,11 +39,16 @@ class proto_decorator(Generic[P]):
|
|
|
36
39
|
_type = type_hints.get(field.name, None)
|
|
37
40
|
if _type:
|
|
38
41
|
value = getattr(self, field.name)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
proto_value = getattr(proto, field.name)
|
|
43
|
+
if issubclass(_type, float) and isinstance(proto_value, AdFloatType):
|
|
44
|
+
proto_value.CopyFrom(_to_ad_proto(value))
|
|
45
|
+
elif issubclass(_type, Vector3):
|
|
46
|
+
if isinstance(proto_value, AdVector3):
|
|
47
|
+
proto_value.CopyFrom(_to_vector3_ad_proto((value.x, value.y, value.z)))
|
|
48
|
+
else:
|
|
49
|
+
proto_value.x = value.x
|
|
50
|
+
proto_value.y = value.y
|
|
51
|
+
proto_value.z = value.z
|
|
44
52
|
else:
|
|
45
53
|
setattr(proto, field.name, value)
|
|
46
54
|
return proto
|
luminarycloud/_helpers/upload.py
CHANGED
|
@@ -3,6 +3,7 @@ from os import PathLike
|
|
|
3
3
|
import os
|
|
4
4
|
import pathlib
|
|
5
5
|
from typing import Mapping
|
|
6
|
+
from urllib.parse import urlparse
|
|
6
7
|
import requests
|
|
7
8
|
import logging
|
|
8
9
|
import grpc
|
|
@@ -15,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def gcs_resumable_upload(
|
|
18
|
-
filepath: PathLike | str, signed_url: str, http_headers: Mapping[str, str]
|
|
19
|
+
client: Client, filepath: PathLike | str, signed_url: str, http_headers: Mapping[str, str]
|
|
19
20
|
) -> None:
|
|
20
21
|
"""
|
|
21
22
|
Performs a resumable upload to a GCS signed url.
|
|
@@ -24,12 +25,17 @@ def gcs_resumable_upload(
|
|
|
24
25
|
"""
|
|
25
26
|
|
|
26
27
|
# initiate the upload
|
|
28
|
+
parsed_url = urlparse(signed_url)
|
|
27
29
|
try:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if parsed_url.hostname and parsed_url.hostname.endswith(".luminarycloud.com"):
|
|
31
|
+
# we must be using the gcsproxy, so we'll use the `client.http` client to authenticate
|
|
32
|
+
# with our backend
|
|
33
|
+
logger.debug("signed url points to LC backend, will use authenticated client")
|
|
34
|
+
post_res = client.http.raw_request("POST", signed_url, headers=http_headers)
|
|
35
|
+
else:
|
|
36
|
+
# we're using the GCS signed url directly, no additional authentication is needed
|
|
37
|
+
logger.debug("signed url points to GCS, will use unauthenticated client")
|
|
38
|
+
post_res = requests.post(url=signed_url, headers=http_headers)
|
|
33
39
|
logger.debug(
|
|
34
40
|
f"sucessfully initialized signed URL upload for {filepath}, POST status code: {post_res.status_code}"
|
|
35
41
|
)
|
|
@@ -49,6 +55,8 @@ def gcs_resumable_upload(
|
|
|
49
55
|
session_uri = post_res.headers["Location"]
|
|
50
56
|
size = os.path.getsize(filepath)
|
|
51
57
|
with open(filepath, "rb") as fp:
|
|
58
|
+
# even if a signed url upload is initiated via the gcsproxy, the subsequent PUT request
|
|
59
|
+
# goes straight to GCS, so we don't want to use the authenticated client here
|
|
52
60
|
put_res = requests.put(
|
|
53
61
|
url=session_uri,
|
|
54
62
|
headers={
|
|
@@ -57,14 +65,11 @@ def gcs_resumable_upload(
|
|
|
57
65
|
data=fp.read(size),
|
|
58
66
|
)
|
|
59
67
|
logger.debug(f"sucessfully uploaded {filepath}, PUT status code: {put_res.status_code}")
|
|
60
|
-
except:
|
|
68
|
+
except Exception as e:
|
|
61
69
|
# don't log the session_uri, just to be safe
|
|
62
|
-
msg =
|
|
63
|
-
f"failed to upload {filepath}, PUT status code: {put_res.status_code}, content: "
|
|
64
|
-
+ str(put_res.content)
|
|
65
|
-
)
|
|
70
|
+
msg = f"failed to upload {filepath}, {e})"
|
|
66
71
|
logger.error(msg)
|
|
67
|
-
raise Exception(
|
|
72
|
+
raise Exception(e)
|
|
68
73
|
|
|
69
74
|
|
|
70
75
|
def upload_file(
|
|
@@ -92,6 +97,7 @@ def upload_file(
|
|
|
92
97
|
logger.debug("started gcs upload")
|
|
93
98
|
|
|
94
99
|
gcs_resumable_upload(
|
|
100
|
+
client=client,
|
|
95
101
|
filepath=pathlib.Path(file_path),
|
|
96
102
|
signed_url=start_res.upload.gcs_resumable.signed_url,
|
|
97
103
|
http_headers=start_res.upload.gcs_resumable.http_headers,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: proto/api/v0/luminarycloud/feature_flag/feature_flag.proto
|
|
4
|
+
"""Generated protocol buffer code."""
|
|
5
|
+
from google.protobuf import descriptor as _descriptor
|
|
6
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
7
|
+
from google.protobuf import message as _message
|
|
8
|
+
from google.protobuf import reflection as _reflection
|
|
9
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2
|
|
16
|
+
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n:proto/api/v0/luminarycloud/feature_flag/feature_flag.proto\x12\x30luminary.proto.api.v0.luminarycloud.feature_flag\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xd9\x01\n\x17GetFeatureFlagsResponse\x12\x81\x01\n\x15\x65nabled_feature_flags\x18\x01 \x03(\x0b\x32\x62.luminary.proto.api.v0.luminarycloud.feature_flag.GetFeatureFlagsResponse.EnabledFeatureFlagsEntry\x1a:\n\x18\x45nabledFeatureFlagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32\xa6\x01\n\x12\x46\x65\x61tureFlagService\x12\x8f\x01\n\x0fGetFeatureFlags\x12\x16.google.protobuf.Empty\x1aI.luminary.proto.api.v0.luminarycloud.feature_flag.GetFeatureFlagsResponse\"\x19\x82\xd3\xe4\x93\x02\x13\x12\x11/v0/feature_flagsB@Z>luminarycloud.com/core/proto/api/v0/luminarycloud/feature_flagb\x06proto3')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_GETFEATUREFLAGSRESPONSE = DESCRIPTOR.message_types_by_name['GetFeatureFlagsResponse']
|
|
24
|
+
_GETFEATUREFLAGSRESPONSE_ENABLEDFEATUREFLAGSENTRY = _GETFEATUREFLAGSRESPONSE.nested_types_by_name['EnabledFeatureFlagsEntry']
|
|
25
|
+
GetFeatureFlagsResponse = _reflection.GeneratedProtocolMessageType('GetFeatureFlagsResponse', (_message.Message,), {
|
|
26
|
+
|
|
27
|
+
'EnabledFeatureFlagsEntry' : _reflection.GeneratedProtocolMessageType('EnabledFeatureFlagsEntry', (_message.Message,), {
|
|
28
|
+
'DESCRIPTOR' : _GETFEATUREFLAGSRESPONSE_ENABLEDFEATUREFLAGSENTRY,
|
|
29
|
+
'__module__' : 'proto.api.v0.luminarycloud.feature_flag.feature_flag_pb2'
|
|
30
|
+
# @@protoc_insertion_point(class_scope:luminary.proto.api.v0.luminarycloud.feature_flag.GetFeatureFlagsResponse.EnabledFeatureFlagsEntry)
|
|
31
|
+
})
|
|
32
|
+
,
|
|
33
|
+
'DESCRIPTOR' : _GETFEATUREFLAGSRESPONSE,
|
|
34
|
+
'__module__' : 'proto.api.v0.luminarycloud.feature_flag.feature_flag_pb2'
|
|
35
|
+
# @@protoc_insertion_point(class_scope:luminary.proto.api.v0.luminarycloud.feature_flag.GetFeatureFlagsResponse)
|
|
36
|
+
})
|
|
37
|
+
_sym_db.RegisterMessage(GetFeatureFlagsResponse)
|
|
38
|
+
_sym_db.RegisterMessage(GetFeatureFlagsResponse.EnabledFeatureFlagsEntry)
|
|
39
|
+
|
|
40
|
+
_FEATUREFLAGSERVICE = DESCRIPTOR.services_by_name['FeatureFlagService']
|
|
41
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
42
|
+
|
|
43
|
+
DESCRIPTOR._options = None
|
|
44
|
+
DESCRIPTOR._serialized_options = b'Z>luminarycloud.com/core/proto/api/v0/luminarycloud/feature_flag'
|
|
45
|
+
_GETFEATUREFLAGSRESPONSE_ENABLEDFEATUREFLAGSENTRY._options = None
|
|
46
|
+
_GETFEATUREFLAGSRESPONSE_ENABLEDFEATUREFLAGSENTRY._serialized_options = b'8\001'
|
|
47
|
+
_FEATUREFLAGSERVICE.methods_by_name['GetFeatureFlags']._options = None
|
|
48
|
+
_FEATUREFLAGSERVICE.methods_by_name['GetFeatureFlags']._serialized_options = b'\202\323\344\223\002\023\022\021/v0/feature_flags'
|
|
49
|
+
_GETFEATUREFLAGSRESPONSE._serialized_start=172
|
|
50
|
+
_GETFEATUREFLAGSRESPONSE._serialized_end=389
|
|
51
|
+
_GETFEATUREFLAGSRESPONSE_ENABLEDFEATUREFLAGSENTRY._serialized_start=331
|
|
52
|
+
_GETFEATUREFLAGSRESPONSE_ENABLEDFEATUREFLAGSENTRY._serialized_end=389
|
|
53
|
+
_FEATUREFLAGSERVICE._serialized_start=392
|
|
54
|
+
_FEATUREFLAGSERVICE._serialized_end=558
|
|
55
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@generated by mypy-protobuf. Do not edit manually!
|
|
3
|
+
isort:skip_file
|
|
4
|
+
"""
|
|
5
|
+
import builtins
|
|
6
|
+
import collections.abc
|
|
7
|
+
import google.protobuf.descriptor
|
|
8
|
+
import google.protobuf.internal.containers
|
|
9
|
+
import google.protobuf.message
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
if sys.version_info >= (3, 8):
|
|
13
|
+
import typing as typing_extensions
|
|
14
|
+
else:
|
|
15
|
+
import typing_extensions
|
|
16
|
+
|
|
17
|
+
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
|
|
18
|
+
|
|
19
|
+
class GetFeatureFlagsResponse(google.protobuf.message.Message):
|
|
20
|
+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
|
21
|
+
|
|
22
|
+
class EnabledFeatureFlagsEntry(google.protobuf.message.Message):
|
|
23
|
+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
|
24
|
+
|
|
25
|
+
KEY_FIELD_NUMBER: builtins.int
|
|
26
|
+
VALUE_FIELD_NUMBER: builtins.int
|
|
27
|
+
key: builtins.int
|
|
28
|
+
value: builtins.str
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
key: builtins.int = ...,
|
|
33
|
+
value: builtins.str = ...,
|
|
34
|
+
) -> None: ...
|
|
35
|
+
def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ...
|
|
36
|
+
|
|
37
|
+
ENABLED_FEATURE_FLAGS_FIELD_NUMBER: builtins.int
|
|
38
|
+
@property
|
|
39
|
+
def enabled_feature_flags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.int, builtins.str]:
|
|
40
|
+
"""Map of enabled feature flags where:
|
|
41
|
+
- Key: experiment ID (uint64)
|
|
42
|
+
- Value: feature flag name (string)
|
|
43
|
+
Returns an empty map if no feature flags are enabled for the user.
|
|
44
|
+
"""
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
enabled_feature_flags: collections.abc.Mapping[builtins.int, builtins.str] | None = ...,
|
|
49
|
+
) -> None: ...
|
|
50
|
+
def ClearField(self, field_name: typing_extensions.Literal["enabled_feature_flags", b"enabled_feature_flags"]) -> None: ...
|
|
51
|
+
|
|
52
|
+
global___GetFeatureFlagsResponse = GetFeatureFlagsResponse
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
|
|
5
|
+
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
|
6
|
+
from luminarycloud._proto.api.v0.luminarycloud.feature_flag import feature_flag_pb2 as proto_dot_api_dot_v0_dot_luminarycloud_dot_feature__flag_dot_feature__flag__pb2
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FeatureFlagServiceStub(object):
|
|
10
|
+
"""Manages feature flags for internal users.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, channel):
|
|
14
|
+
"""Constructor.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
channel: A grpc.Channel.
|
|
18
|
+
"""
|
|
19
|
+
self.GetFeatureFlags = channel.unary_unary(
|
|
20
|
+
'/luminary.proto.api.v0.luminarycloud.feature_flag.FeatureFlagService/GetFeatureFlags',
|
|
21
|
+
request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
|
|
22
|
+
response_deserializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_feature__flag_dot_feature__flag__pb2.GetFeatureFlagsResponse.FromString,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FeatureFlagServiceServicer(object):
|
|
27
|
+
"""Manages feature flags for internal users.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def GetFeatureFlags(self, request, context):
|
|
31
|
+
"""Gets all enabled feature flags for the authenticated user.
|
|
32
|
+
Feature flags are used to control access to experimental and internal features.
|
|
33
|
+
"""
|
|
34
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
35
|
+
context.set_details('Method not implemented!')
|
|
36
|
+
raise NotImplementedError('Method not implemented!')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def add_FeatureFlagServiceServicer_to_server(servicer, server):
|
|
40
|
+
rpc_method_handlers = {
|
|
41
|
+
'GetFeatureFlags': grpc.unary_unary_rpc_method_handler(
|
|
42
|
+
servicer.GetFeatureFlags,
|
|
43
|
+
request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
|
|
44
|
+
response_serializer=proto_dot_api_dot_v0_dot_luminarycloud_dot_feature__flag_dot_feature__flag__pb2.GetFeatureFlagsResponse.SerializeToString,
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
generic_handler = grpc.method_handlers_generic_handler(
|
|
48
|
+
'luminary.proto.api.v0.luminarycloud.feature_flag.FeatureFlagService', rpc_method_handlers)
|
|
49
|
+
server.add_generic_rpc_handlers((generic_handler,))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# This class is part of an EXPERIMENTAL API.
|
|
53
|
+
class FeatureFlagService(object):
|
|
54
|
+
"""Manages feature flags for internal users.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def GetFeatureFlags(request,
|
|
59
|
+
target,
|
|
60
|
+
options=(),
|
|
61
|
+
channel_credentials=None,
|
|
62
|
+
call_credentials=None,
|
|
63
|
+
insecure=False,
|
|
64
|
+
compression=None,
|
|
65
|
+
wait_for_ready=None,
|
|
66
|
+
timeout=None,
|
|
67
|
+
metadata=None):
|
|
68
|
+
return grpc.experimental.unary_unary(request, target, '/luminary.proto.api.v0.luminarycloud.feature_flag.FeatureFlagService/GetFeatureFlags',
|
|
69
|
+
google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
|
|
70
|
+
proto_dot_api_dot_v0_dot_luminarycloud_dot_feature__flag_dot_feature__flag__pb2.GetFeatureFlagsResponse.FromString,
|
|
71
|
+
options, channel_credentials,
|
|
72
|
+
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@generated by mypy-protobuf. Do not edit manually!
|
|
3
|
+
isort:skip_file
|
|
4
|
+
"""
|
|
5
|
+
import abc
|
|
6
|
+
import google.protobuf.empty_pb2
|
|
7
|
+
import grpc
|
|
8
|
+
import luminarycloud._proto.api.v0.luminarycloud.feature_flag.feature_flag_pb2
|
|
9
|
+
|
|
10
|
+
class FeatureFlagServiceStub:
|
|
11
|
+
"""Manages feature flags for internal users."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, channel: grpc.Channel) -> None: ...
|
|
14
|
+
GetFeatureFlags: grpc.UnaryUnaryMultiCallable[
|
|
15
|
+
google.protobuf.empty_pb2.Empty,
|
|
16
|
+
luminarycloud._proto.api.v0.luminarycloud.feature_flag.feature_flag_pb2.GetFeatureFlagsResponse,
|
|
17
|
+
]
|
|
18
|
+
"""Gets all enabled feature flags for the authenticated user.
|
|
19
|
+
Feature flags are used to control access to experimental and internal features.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
class FeatureFlagServiceServicer(metaclass=abc.ABCMeta):
|
|
23
|
+
"""Manages feature flags for internal users."""
|
|
24
|
+
|
|
25
|
+
@abc.abstractmethod
|
|
26
|
+
def GetFeatureFlags(
|
|
27
|
+
self,
|
|
28
|
+
request: google.protobuf.empty_pb2.Empty,
|
|
29
|
+
context: grpc.ServicerContext,
|
|
30
|
+
) -> luminarycloud._proto.api.v0.luminarycloud.feature_flag.feature_flag_pb2.GetFeatureFlagsResponse:
|
|
31
|
+
"""Gets all enabled feature flags for the authenticated user.
|
|
32
|
+
Feature flags are used to control access to experimental and internal features.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def add_FeatureFlagServiceServicer_to_server(servicer: FeatureFlagServiceServicer, server: grpc.Server) -> None: ...
|