flyte 0.2.0b1__py3-none-any.whl → 2.0.0b46__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.
- flyte/__init__.py +83 -30
- flyte/_bin/connect.py +61 -0
- flyte/_bin/debug.py +38 -0
- flyte/_bin/runtime.py +87 -19
- flyte/_bin/serve.py +351 -0
- flyte/_build.py +3 -2
- flyte/_cache/cache.py +6 -5
- flyte/_cache/local_cache.py +216 -0
- flyte/_code_bundle/_ignore.py +31 -5
- flyte/_code_bundle/_packaging.py +42 -11
- flyte/_code_bundle/_utils.py +57 -34
- flyte/_code_bundle/bundle.py +130 -27
- flyte/_constants.py +1 -0
- flyte/_context.py +21 -5
- flyte/_custom_context.py +73 -0
- flyte/_debug/constants.py +37 -0
- flyte/_debug/utils.py +17 -0
- flyte/_debug/vscode.py +315 -0
- flyte/_deploy.py +396 -75
- flyte/_deployer.py +109 -0
- flyte/_environment.py +94 -11
- flyte/_excepthook.py +37 -0
- flyte/_group.py +2 -1
- flyte/_hash.py +1 -16
- flyte/_image.py +544 -231
- flyte/_initialize.py +456 -316
- flyte/_interface.py +40 -5
- flyte/_internal/controllers/__init__.py +22 -8
- flyte/_internal/controllers/_local_controller.py +159 -35
- flyte/_internal/controllers/_trace.py +18 -10
- flyte/_internal/controllers/remote/__init__.py +38 -9
- flyte/_internal/controllers/remote/_action.py +82 -12
- flyte/_internal/controllers/remote/_client.py +6 -2
- flyte/_internal/controllers/remote/_controller.py +290 -64
- flyte/_internal/controllers/remote/_core.py +155 -95
- flyte/_internal/controllers/remote/_informer.py +40 -20
- flyte/_internal/controllers/remote/_service_protocol.py +2 -2
- flyte/_internal/imagebuild/__init__.py +2 -10
- flyte/_internal/imagebuild/docker_builder.py +391 -84
- flyte/_internal/imagebuild/image_builder.py +111 -55
- flyte/_internal/imagebuild/remote_builder.py +409 -0
- flyte/_internal/imagebuild/utils.py +79 -0
- flyte/_internal/resolvers/_app_env_module.py +92 -0
- flyte/_internal/resolvers/_task_module.py +5 -38
- flyte/_internal/resolvers/app_env.py +26 -0
- flyte/_internal/resolvers/common.py +8 -1
- flyte/_internal/resolvers/default.py +2 -2
- flyte/_internal/runtime/convert.py +319 -36
- flyte/_internal/runtime/entrypoints.py +106 -18
- flyte/_internal/runtime/io.py +71 -23
- flyte/_internal/runtime/resources_serde.py +21 -7
- flyte/_internal/runtime/reuse.py +125 -0
- flyte/_internal/runtime/rusty.py +196 -0
- flyte/_internal/runtime/task_serde.py +239 -66
- flyte/_internal/runtime/taskrunner.py +48 -8
- flyte/_internal/runtime/trigger_serde.py +162 -0
- flyte/_internal/runtime/types_serde.py +7 -16
- flyte/_keyring/file.py +115 -0
- flyte/_link.py +30 -0
- flyte/_logging.py +241 -42
- flyte/_map.py +312 -0
- flyte/_metrics.py +59 -0
- flyte/_module.py +74 -0
- flyte/_pod.py +30 -0
- flyte/_resources.py +296 -33
- flyte/_retry.py +1 -7
- flyte/_reusable_environment.py +72 -7
- flyte/_run.py +462 -132
- flyte/_secret.py +47 -11
- flyte/_serve.py +333 -0
- flyte/_task.py +245 -56
- flyte/_task_environment.py +219 -97
- flyte/_task_plugins.py +47 -0
- flyte/_tools.py +8 -8
- flyte/_trace.py +15 -24
- flyte/_trigger.py +1027 -0
- flyte/_utils/__init__.py +12 -1
- flyte/_utils/asyn.py +3 -1
- flyte/_utils/async_cache.py +139 -0
- flyte/_utils/coro_management.py +5 -4
- flyte/_utils/description_parser.py +19 -0
- flyte/_utils/docker_credentials.py +173 -0
- flyte/_utils/helpers.py +45 -19
- flyte/_utils/module_loader.py +123 -0
- flyte/_utils/org_discovery.py +57 -0
- flyte/_utils/uv_script_parser.py +8 -1
- flyte/_version.py +16 -3
- flyte/app/__init__.py +27 -0
- flyte/app/_app_environment.py +362 -0
- flyte/app/_connector_environment.py +40 -0
- flyte/app/_deploy.py +130 -0
- flyte/app/_parameter.py +343 -0
- flyte/app/_runtime/__init__.py +3 -0
- flyte/app/_runtime/app_serde.py +383 -0
- flyte/app/_types.py +113 -0
- flyte/app/extras/__init__.py +9 -0
- flyte/app/extras/_auth_middleware.py +217 -0
- flyte/app/extras/_fastapi.py +93 -0
- flyte/app/extras/_model_loader/__init__.py +3 -0
- flyte/app/extras/_model_loader/config.py +7 -0
- flyte/app/extras/_model_loader/loader.py +288 -0
- flyte/cli/__init__.py +12 -0
- flyte/cli/_abort.py +28 -0
- flyte/cli/_build.py +114 -0
- flyte/cli/_common.py +493 -0
- flyte/cli/_create.py +371 -0
- flyte/cli/_delete.py +45 -0
- flyte/cli/_deploy.py +401 -0
- flyte/cli/_gen.py +316 -0
- flyte/cli/_get.py +446 -0
- flyte/cli/_option.py +33 -0
- flyte/{_cli → cli}/_params.py +57 -17
- flyte/cli/_plugins.py +209 -0
- flyte/cli/_prefetch.py +292 -0
- flyte/cli/_run.py +690 -0
- flyte/cli/_serve.py +338 -0
- flyte/cli/_update.py +86 -0
- flyte/cli/_user.py +20 -0
- flyte/cli/main.py +246 -0
- flyte/config/__init__.py +2 -167
- flyte/config/_config.py +215 -163
- flyte/config/_internal.py +10 -1
- flyte/config/_reader.py +225 -0
- flyte/connectors/__init__.py +11 -0
- flyte/connectors/_connector.py +330 -0
- flyte/connectors/_server.py +194 -0
- flyte/connectors/utils.py +159 -0
- flyte/errors.py +134 -2
- flyte/extend.py +24 -0
- flyte/extras/_container.py +69 -56
- flyte/git/__init__.py +3 -0
- flyte/git/_config.py +279 -0
- flyte/io/__init__.py +8 -1
- flyte/io/{structured_dataset → _dataframe}/__init__.py +32 -30
- flyte/io/{structured_dataset → _dataframe}/basic_dfs.py +75 -68
- flyte/io/{structured_dataset/structured_dataset.py → _dataframe/dataframe.py} +207 -242
- flyte/io/_dir.py +575 -113
- flyte/io/_file.py +587 -141
- flyte/io/_hashing_io.py +342 -0
- flyte/io/extend.py +7 -0
- flyte/models.py +635 -0
- flyte/prefetch/__init__.py +22 -0
- flyte/prefetch/_hf_model.py +563 -0
- flyte/remote/__init__.py +14 -3
- flyte/remote/_action.py +879 -0
- flyte/remote/_app.py +346 -0
- flyte/remote/_auth_metadata.py +42 -0
- flyte/remote/_client/_protocols.py +62 -4
- flyte/remote/_client/auth/_auth_utils.py +19 -0
- flyte/remote/_client/auth/_authenticators/base.py +8 -2
- flyte/remote/_client/auth/_authenticators/device_code.py +4 -5
- flyte/remote/_client/auth/_authenticators/factory.py +4 -0
- flyte/remote/_client/auth/_authenticators/passthrough.py +79 -0
- flyte/remote/_client/auth/_authenticators/pkce.py +17 -18
- flyte/remote/_client/auth/_channel.py +47 -18
- flyte/remote/_client/auth/_client_config.py +5 -3
- flyte/remote/_client/auth/_keyring.py +15 -2
- flyte/remote/_client/auth/_token_client.py +3 -3
- flyte/remote/_client/controlplane.py +206 -18
- flyte/remote/_common.py +66 -0
- flyte/remote/_data.py +107 -22
- flyte/remote/_logs.py +116 -33
- flyte/remote/_project.py +21 -19
- flyte/remote/_run.py +164 -631
- flyte/remote/_secret.py +72 -29
- flyte/remote/_task.py +387 -46
- flyte/remote/_trigger.py +368 -0
- flyte/remote/_user.py +43 -0
- flyte/report/_report.py +10 -6
- flyte/storage/__init__.py +13 -1
- flyte/storage/_config.py +237 -0
- flyte/storage/_parallel_reader.py +289 -0
- flyte/storage/_storage.py +268 -59
- flyte/syncify/__init__.py +56 -0
- flyte/syncify/_api.py +414 -0
- flyte/types/__init__.py +39 -0
- flyte/types/_interface.py +22 -7
- flyte/{io/pickle/transformer.py → types/_pickle.py} +37 -9
- flyte/types/_string_literals.py +8 -9
- flyte/types/_type_engine.py +226 -126
- flyte/types/_utils.py +1 -1
- flyte-2.0.0b46.data/scripts/debug.py +38 -0
- flyte-2.0.0b46.data/scripts/runtime.py +194 -0
- flyte-2.0.0b46.dist-info/METADATA +352 -0
- flyte-2.0.0b46.dist-info/RECORD +221 -0
- flyte-2.0.0b46.dist-info/entry_points.txt +8 -0
- flyte-2.0.0b46.dist-info/licenses/LICENSE +201 -0
- flyte/_api_commons.py +0 -3
- flyte/_cli/_common.py +0 -299
- flyte/_cli/_create.py +0 -42
- flyte/_cli/_delete.py +0 -23
- flyte/_cli/_deploy.py +0 -140
- flyte/_cli/_get.py +0 -235
- flyte/_cli/_run.py +0 -174
- flyte/_cli/main.py +0 -98
- flyte/_datastructures.py +0 -342
- flyte/_internal/controllers/pbhash.py +0 -39
- flyte/_protos/common/authorization_pb2.py +0 -66
- flyte/_protos/common/authorization_pb2.pyi +0 -108
- flyte/_protos/common/authorization_pb2_grpc.py +0 -4
- flyte/_protos/common/identifier_pb2.py +0 -71
- flyte/_protos/common/identifier_pb2.pyi +0 -82
- flyte/_protos/common/identifier_pb2_grpc.py +0 -4
- flyte/_protos/common/identity_pb2.py +0 -48
- flyte/_protos/common/identity_pb2.pyi +0 -72
- flyte/_protos/common/identity_pb2_grpc.py +0 -4
- flyte/_protos/common/list_pb2.py +0 -36
- flyte/_protos/common/list_pb2.pyi +0 -69
- flyte/_protos/common/list_pb2_grpc.py +0 -4
- flyte/_protos/common/policy_pb2.py +0 -37
- flyte/_protos/common/policy_pb2.pyi +0 -27
- flyte/_protos/common/policy_pb2_grpc.py +0 -4
- flyte/_protos/common/role_pb2.py +0 -37
- flyte/_protos/common/role_pb2.pyi +0 -53
- flyte/_protos/common/role_pb2_grpc.py +0 -4
- flyte/_protos/common/runtime_version_pb2.py +0 -28
- flyte/_protos/common/runtime_version_pb2.pyi +0 -24
- flyte/_protos/common/runtime_version_pb2_grpc.py +0 -4
- flyte/_protos/logs/dataplane/payload_pb2.py +0 -96
- flyte/_protos/logs/dataplane/payload_pb2.pyi +0 -168
- flyte/_protos/logs/dataplane/payload_pb2_grpc.py +0 -4
- flyte/_protos/secret/definition_pb2.py +0 -49
- flyte/_protos/secret/definition_pb2.pyi +0 -93
- flyte/_protos/secret/definition_pb2_grpc.py +0 -4
- flyte/_protos/secret/payload_pb2.py +0 -62
- flyte/_protos/secret/payload_pb2.pyi +0 -94
- flyte/_protos/secret/payload_pb2_grpc.py +0 -4
- flyte/_protos/secret/secret_pb2.py +0 -38
- flyte/_protos/secret/secret_pb2.pyi +0 -6
- flyte/_protos/secret/secret_pb2_grpc.py +0 -198
- flyte/_protos/secret/secret_pb2_grpc_grpc.py +0 -198
- flyte/_protos/validate/validate/validate_pb2.py +0 -76
- flyte/_protos/workflow/node_execution_service_pb2.py +0 -26
- flyte/_protos/workflow/node_execution_service_pb2.pyi +0 -4
- flyte/_protos/workflow/node_execution_service_pb2_grpc.py +0 -32
- flyte/_protos/workflow/queue_service_pb2.py +0 -106
- flyte/_protos/workflow/queue_service_pb2.pyi +0 -141
- flyte/_protos/workflow/queue_service_pb2_grpc.py +0 -172
- flyte/_protos/workflow/run_definition_pb2.py +0 -128
- flyte/_protos/workflow/run_definition_pb2.pyi +0 -310
- flyte/_protos/workflow/run_definition_pb2_grpc.py +0 -4
- flyte/_protos/workflow/run_logs_service_pb2.py +0 -41
- flyte/_protos/workflow/run_logs_service_pb2.pyi +0 -28
- flyte/_protos/workflow/run_logs_service_pb2_grpc.py +0 -69
- flyte/_protos/workflow/run_service_pb2.py +0 -133
- flyte/_protos/workflow/run_service_pb2.pyi +0 -175
- flyte/_protos/workflow/run_service_pb2_grpc.py +0 -412
- flyte/_protos/workflow/state_service_pb2.py +0 -58
- flyte/_protos/workflow/state_service_pb2.pyi +0 -71
- flyte/_protos/workflow/state_service_pb2_grpc.py +0 -138
- flyte/_protos/workflow/task_definition_pb2.py +0 -72
- flyte/_protos/workflow/task_definition_pb2.pyi +0 -65
- flyte/_protos/workflow/task_definition_pb2_grpc.py +0 -4
- flyte/_protos/workflow/task_service_pb2.py +0 -44
- flyte/_protos/workflow/task_service_pb2.pyi +0 -31
- flyte/_protos/workflow/task_service_pb2_grpc.py +0 -104
- flyte/io/_dataframe.py +0 -0
- flyte/io/pickle/__init__.py +0 -0
- flyte/remote/_console.py +0 -18
- flyte-0.2.0b1.dist-info/METADATA +0 -179
- flyte-0.2.0b1.dist-info/RECORD +0 -204
- flyte-0.2.0b1.dist-info/entry_points.txt +0 -3
- /flyte/{_cli → _debug}/__init__.py +0 -0
- /flyte/{_protos → _keyring}/__init__.py +0 -0
- {flyte-0.2.0b1.dist-info → flyte-2.0.0b46.dist-info}/WHEEL +0 -0
- {flyte-0.2.0b1.dist-info → flyte-2.0.0b46.dist-info}/top_level.txt +0 -0
|
@@ -17,7 +17,6 @@ from urllib.parse import urlencode as _urlencode
|
|
|
17
17
|
import click
|
|
18
18
|
import httpx
|
|
19
19
|
import pydantic
|
|
20
|
-
from h11 import Response
|
|
21
20
|
|
|
22
21
|
from flyte._logging import logger
|
|
23
22
|
from flyte.remote._client.auth._authenticators.base import Authenticator
|
|
@@ -123,7 +122,7 @@ class PKCEAuthenticator(Authenticator):
|
|
|
123
122
|
try:
|
|
124
123
|
return await self._auth_client.refresh_access_token(self._creds)
|
|
125
124
|
except AccessTokenNotFoundError:
|
|
126
|
-
logger.warning("
|
|
125
|
+
logger.warning("Logging in...")
|
|
127
126
|
|
|
128
127
|
return await self._auth_client.get_creds_from_remote()
|
|
129
128
|
|
|
@@ -363,19 +362,17 @@ class AuthorizationClient(object):
|
|
|
363
362
|
|
|
364
363
|
data.update(self._refresh_access_token_params)
|
|
365
364
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
follow_redirects=False,
|
|
373
|
-
),
|
|
374
|
-
) as resp:
|
|
375
|
-
if resp.status_code != _StatusCodes.OK:
|
|
376
|
-
raise AccessTokenNotFoundError(f"Non-200 returned from refresh token endpoint {resp.status_code}")
|
|
365
|
+
resp: httpx.Response = await self._http_session.post(
|
|
366
|
+
url=self._token_endpoint,
|
|
367
|
+
data=data,
|
|
368
|
+
headers=self._headers,
|
|
369
|
+
follow_redirects=False,
|
|
370
|
+
)
|
|
377
371
|
|
|
378
|
-
|
|
372
|
+
if resp.status_code != _StatusCodes.OK:
|
|
373
|
+
raise AccessTokenNotFoundError(f"Non-200 returned from refresh token endpoint {resp.status_code}")
|
|
374
|
+
|
|
375
|
+
return await self._credentials_from_response(resp)
|
|
379
376
|
|
|
380
377
|
|
|
381
378
|
class OAuthCallbackHandler:
|
|
@@ -410,10 +407,12 @@ class OAuthCallbackHandler:
|
|
|
410
407
|
:param reader: The StreamReader for reading the incoming request
|
|
411
408
|
:param writer: The StreamWriter for writing the response
|
|
412
409
|
"""
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
410
|
+
# Read only the first line of the HTTP request (e.g., "GET /callback?code=...&state=... HTTP/1.1")
|
|
411
|
+
# Using readline() instead of read() because read() waits for EOF, which won't come
|
|
412
|
+
# until the client closes the connection - but the client is waiting for our response first.
|
|
413
|
+
request_line = await reader.readline()
|
|
414
|
+
# request_line looks like this: 'GET /callback?code=ABC&state=FOO HTTP/1.1'
|
|
415
|
+
path = request_line.decode().split(" ")[1]
|
|
417
416
|
url = _urlparse.urlparse(path)
|
|
418
417
|
if url.path.strip("/") == self.redirect_path.strip("/"):
|
|
419
418
|
response = f"HTTP/1.1 {_StatusCodes.OK.value} {_StatusCodes.OK.phrase}\r\n"
|
|
@@ -7,6 +7,7 @@ import httpx
|
|
|
7
7
|
from grpc.experimental.aio import init_grpc_aio
|
|
8
8
|
|
|
9
9
|
from flyte._logging import logger
|
|
10
|
+
from flyte._utils.org_discovery import hostname_from_url
|
|
10
11
|
|
|
11
12
|
from ._authenticators.base import get_async_session
|
|
12
13
|
from ._authenticators.factory import (
|
|
@@ -30,21 +31,26 @@ def bootstrap_ssl_from_server(endpoint: str) -> grpc.ChannelCredentials:
|
|
|
30
31
|
:param endpoint: The endpoint URL to retrieve the SSL certificate from, may include port number
|
|
31
32
|
:return: gRPC channel credentials created from the retrieved certificate
|
|
32
33
|
"""
|
|
34
|
+
hostname = hostname_from_url(endpoint)
|
|
35
|
+
|
|
33
36
|
# Get port from endpoint or use 443
|
|
34
|
-
endpoint_parts =
|
|
37
|
+
endpoint_parts = hostname.rsplit(":", 1)
|
|
35
38
|
if len(endpoint_parts) == 2 and endpoint_parts[1].isdigit():
|
|
36
39
|
server_address = (endpoint_parts[0], int(endpoint_parts[1]))
|
|
37
40
|
else:
|
|
38
|
-
logger.warning(f"Unrecognized port in endpoint [{
|
|
39
|
-
server_address = (
|
|
41
|
+
logger.warning(f"Unrecognized port in endpoint [{hostname}], defaulting to 443.")
|
|
42
|
+
server_address = (hostname, 443)
|
|
40
43
|
|
|
41
|
-
# Run the blocking SSL certificate retrieval
|
|
42
|
-
|
|
44
|
+
# Run the blocking SSL certificate retrieval with a timeout
|
|
45
|
+
logger.debug(f"Retrieving SSL certificate from {server_address}")
|
|
46
|
+
cert = ssl.get_server_certificate(server_address, timeout=10)
|
|
43
47
|
return grpc.ssl_channel_credentials(str.encode(cert))
|
|
44
48
|
|
|
45
49
|
|
|
46
50
|
async def create_channel(
|
|
47
|
-
endpoint: str,
|
|
51
|
+
endpoint: str | None,
|
|
52
|
+
api_key: str | None = None,
|
|
53
|
+
/,
|
|
48
54
|
insecure: typing.Optional[bool] = None,
|
|
49
55
|
insecure_skip_verify: typing.Optional[bool] = False,
|
|
50
56
|
ca_cert_file_path: typing.Optional[str] = None,
|
|
@@ -66,6 +72,7 @@ async def create_channel(
|
|
|
66
72
|
and create authentication interceptors that perform async operations.
|
|
67
73
|
|
|
68
74
|
:param endpoint: The endpoint URL for the gRPC channel
|
|
75
|
+
:param api_key: API key for authentication; if provided, it will be used to detect the endpoint and credentials.
|
|
69
76
|
:param insecure: Whether to use an insecure channel (no SSL)
|
|
70
77
|
:param insecure_skip_verify: Whether to skip SSL certificate verification
|
|
71
78
|
:param ca_cert_file_path: Path to CA certificate file for SSL verification
|
|
@@ -104,23 +111,40 @@ async def create_channel(
|
|
|
104
111
|
- refresh_access_token_params: Parameters to add when refreshing access token
|
|
105
112
|
:return: grpc.aio.Channel with authentication interceptors configured
|
|
106
113
|
"""
|
|
114
|
+
assert endpoint or api_key, "Either endpoint or api_key must be specified"
|
|
115
|
+
|
|
116
|
+
if api_key:
|
|
117
|
+
from flyte.remote._client.auth._auth_utils import decode_api_key
|
|
107
118
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
endpoint, client_id, client_secret, _org = decode_api_key(api_key)
|
|
120
|
+
kwargs["auth_type"] = "ClientSecret"
|
|
121
|
+
kwargs["client_id"] = client_id
|
|
122
|
+
kwargs["client_secret"] = client_secret
|
|
123
|
+
kwargs["client_credentials_secret"] = client_secret
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
st_cert = f.read()
|
|
116
|
-
ssl_credentials = grpc.ssl_channel_credentials(st_cert)
|
|
117
|
-
else:
|
|
118
|
-
ssl_credentials = grpc.ssl_channel_credentials()
|
|
125
|
+
assert endpoint, "Endpoint must be specified by this point"
|
|
119
126
|
|
|
120
127
|
# Create an unauthenticated channel first to use to get the server metadata
|
|
121
128
|
if insecure:
|
|
122
|
-
|
|
129
|
+
insecure_kwargs = {}
|
|
130
|
+
if kw_opts := kwargs.get("options"):
|
|
131
|
+
insecure_kwargs["options"] = kw_opts
|
|
132
|
+
if compression:
|
|
133
|
+
insecure_kwargs["compression"] = compression
|
|
134
|
+
unauthenticated_channel = grpc.aio.insecure_channel(endpoint, **insecure_kwargs)
|
|
123
135
|
else:
|
|
136
|
+
# Only create SSL credentials if not provided and also only when using secure channel.
|
|
137
|
+
if not ssl_credentials:
|
|
138
|
+
if insecure_skip_verify:
|
|
139
|
+
ssl_credentials = bootstrap_ssl_from_server(endpoint)
|
|
140
|
+
elif ca_cert_file_path:
|
|
141
|
+
import aiofiles
|
|
142
|
+
|
|
143
|
+
async with aiofiles.open(ca_cert_file_path, "rb") as f:
|
|
144
|
+
st_cert = await f.read()
|
|
145
|
+
ssl_credentials = grpc.ssl_channel_credentials(st_cert)
|
|
146
|
+
else:
|
|
147
|
+
ssl_credentials = grpc.ssl_channel_credentials()
|
|
124
148
|
unauthenticated_channel = grpc.aio.secure_channel(
|
|
125
149
|
target=endpoint,
|
|
126
150
|
credentials=ssl_credentials,
|
|
@@ -173,7 +197,12 @@ async def create_channel(
|
|
|
173
197
|
interceptors.extend(auth_interceptors)
|
|
174
198
|
|
|
175
199
|
if insecure:
|
|
176
|
-
|
|
200
|
+
insecure_kwargs = {}
|
|
201
|
+
if kw_opts := kwargs.get("options"):
|
|
202
|
+
insecure_kwargs["options"] = kw_opts
|
|
203
|
+
if compression:
|
|
204
|
+
insecure_kwargs["compression"] = compression
|
|
205
|
+
return grpc.aio.insecure_channel(endpoint, interceptors=interceptors, **insecure_kwargs)
|
|
177
206
|
|
|
178
207
|
return grpc.aio.secure_channel(
|
|
179
208
|
target=endpoint,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import typing
|
|
2
3
|
from abc import abstractmethod
|
|
3
4
|
|
|
@@ -6,7 +7,7 @@ import pydantic
|
|
|
6
7
|
from flyteidl.service.auth_pb2 import OAuth2MetadataRequest, PublicClientAuthConfigRequest
|
|
7
8
|
from flyteidl.service.auth_pb2_grpc import AuthMetadataServiceStub
|
|
8
9
|
|
|
9
|
-
AuthType = typing.Literal["ClientSecret", "Pkce", "ExternalCommand", "DeviceFlow"]
|
|
10
|
+
AuthType = typing.Literal["ClientSecret", "Pkce", "ExternalCommand", "DeviceFlow", "Passthrough"]
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class ClientConfig(pydantic.BaseModel):
|
|
@@ -69,8 +70,9 @@ class RemoteClientConfigStore(ClientConfigStore):
|
|
|
69
70
|
Retrieves the ClientConfig from the given grpc.Channel assuming AuthMetadataService is available
|
|
70
71
|
"""
|
|
71
72
|
metadata_service = AuthMetadataServiceStub(self._unauthenticated_channel)
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
oauth2_metadata_task = metadata_service.GetOAuth2Metadata(OAuth2MetadataRequest())
|
|
74
|
+
public_client_config_task = metadata_service.GetPublicClientConfig(PublicClientAuthConfigRequest())
|
|
75
|
+
oauth2_metadata, public_client_config = await asyncio.gather(oauth2_metadata_task, public_client_config_task)
|
|
74
76
|
return ClientConfig(
|
|
75
77
|
token_endpoint=oauth2_metadata.token_endpoint,
|
|
76
78
|
authorization_endpoint=oauth2_metadata.authorization_endpoint,
|
|
@@ -81,6 +81,8 @@ class KeyringStore:
|
|
|
81
81
|
)
|
|
82
82
|
except NoKeyringError as e:
|
|
83
83
|
logger.debug(f"KeyRing not available, tokens will not be cached. Error: {e}")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.debug(f"Failed to store tokens in keyring. Error: {e}")
|
|
84
86
|
return credentials
|
|
85
87
|
|
|
86
88
|
@staticmethod
|
|
@@ -96,16 +98,23 @@ class KeyringStore:
|
|
|
96
98
|
or if the system keyring is not available
|
|
97
99
|
"""
|
|
98
100
|
for_endpoint = strip_scheme(for_endpoint)
|
|
101
|
+
access_token: str | None = None
|
|
99
102
|
try:
|
|
100
103
|
refresh_token = keyring.get_password(for_endpoint, KeyringStore._refresh_token_key)
|
|
101
104
|
access_token = keyring.get_password(for_endpoint, KeyringStore._access_token_key)
|
|
102
105
|
except NoKeyringError as e:
|
|
103
106
|
logger.debug(f"KeyRing not available, tokens will not be cached. Error: {e}")
|
|
104
107
|
return None
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.debug(f"Failed to retrieve tokens from keyring. Error: {e}")
|
|
110
|
+
return None
|
|
105
111
|
|
|
106
112
|
if not access_token:
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
if not refresh_token:
|
|
114
|
+
logger.debug("No access token found in keyring.")
|
|
115
|
+
return None
|
|
116
|
+
else:
|
|
117
|
+
access_token = ""
|
|
109
118
|
|
|
110
119
|
return Credentials(
|
|
111
120
|
access_token=access_token,
|
|
@@ -138,6 +147,10 @@ class KeyringStore:
|
|
|
138
147
|
logger.debug(f"Key {key} not found in key store, Ignoring. Error: {e}")
|
|
139
148
|
except NoKeyringError as e:
|
|
140
149
|
logger.debug(f"KeyRing not available, Key {key} deletion failed. Error: {e}")
|
|
150
|
+
except NotImplementedError as e:
|
|
151
|
+
logger.debug(f"Key {key} deletion not implemented in keyring backend. Error: {e}")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.debug(f"Failed to delete key {key} from keyring. Error: {e}")
|
|
141
154
|
|
|
142
155
|
_delete_key(KeyringStore._access_token_key)
|
|
143
156
|
_delete_key(KeyringStore._refresh_token_key)
|
|
@@ -94,7 +94,7 @@ async def get_token(
|
|
|
94
94
|
http_proxy_url: typing.Optional[str] = None,
|
|
95
95
|
verify: typing.Optional[typing.Union[bool, str]] = None,
|
|
96
96
|
refresh_token: typing.Optional[str] = None,
|
|
97
|
-
) -> typing.Tuple[str, str, int]:
|
|
97
|
+
) -> typing.Tuple[str, str | None, int]:
|
|
98
98
|
"""
|
|
99
99
|
Retrieves an access token from the specified token endpoint.
|
|
100
100
|
|
|
@@ -165,7 +165,7 @@ async def get_token(
|
|
|
165
165
|
if "refresh_token" in j:
|
|
166
166
|
new_refresh_token = j["refresh_token"]
|
|
167
167
|
else:
|
|
168
|
-
|
|
168
|
+
logger.info("No refresh token received, this is expected for client credentials flow")
|
|
169
169
|
|
|
170
170
|
return j["access_token"], new_refresh_token, j["expires_in"]
|
|
171
171
|
|
|
@@ -213,7 +213,7 @@ async def poll_token_endpoint(
|
|
|
213
213
|
scopes: typing.Optional[typing.List[str]] = None,
|
|
214
214
|
http_proxy_url: typing.Optional[str] = None,
|
|
215
215
|
verify: typing.Optional[typing.Union[bool, str]] = None,
|
|
216
|
-
) -> typing.Tuple[str, str, int]:
|
|
216
|
+
) -> typing.Tuple[str, str | None, int]:
|
|
217
217
|
"""
|
|
218
218
|
Polls the token endpoint until authentication is complete or times out.
|
|
219
219
|
|
|
@@ -1,59 +1,218 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from
|
|
3
|
+
import os
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
# Set environment variables for gRPC, this reduces log spew and avoids unnecessary warnings
|
|
7
|
+
# before importing grpc
|
|
8
|
+
if "GRPC_VERBOSITY" not in os.environ:
|
|
9
|
+
os.environ["GRPC_VERBOSITY"] = "ERROR"
|
|
10
|
+
os.environ["GRPC_CPP_MIN_LOG_LEVEL"] = "ERROR"
|
|
11
|
+
# Disable fork support (stops "skipping fork() handlers")
|
|
12
|
+
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "0"
|
|
13
|
+
# Reduce absl/glog verbosity
|
|
14
|
+
os.environ["GLOG_minloglevel"] = "2"
|
|
15
|
+
os.environ["ABSL_LOG"] = "0"
|
|
16
|
+
#### Has to be before grpc
|
|
5
17
|
|
|
6
|
-
|
|
7
|
-
from
|
|
18
|
+
import grpc
|
|
19
|
+
from flyteidl.service import admin_pb2_grpc, dataproxy_pb2_grpc, identity_pb2_grpc
|
|
20
|
+
from flyteidl2.app import app_service_pb2_grpc
|
|
21
|
+
from flyteidl2.secret import secret_pb2_grpc
|
|
22
|
+
from flyteidl2.task import task_service_pb2_grpc
|
|
23
|
+
from flyteidl2.trigger import trigger_service_pb2_grpc
|
|
24
|
+
from flyteidl2.workflow import run_logs_service_pb2_grpc, run_service_pb2_grpc
|
|
8
25
|
|
|
9
26
|
from ._protocols import (
|
|
27
|
+
AppService,
|
|
10
28
|
DataProxyService,
|
|
29
|
+
IdentityService,
|
|
11
30
|
MetadataServiceProtocol,
|
|
12
31
|
ProjectDomainService,
|
|
13
32
|
RunLogsService,
|
|
14
33
|
RunService,
|
|
15
34
|
SecretService,
|
|
16
35
|
TaskService,
|
|
36
|
+
TriggerService,
|
|
17
37
|
)
|
|
18
38
|
from .auth import create_channel
|
|
19
39
|
|
|
20
40
|
|
|
41
|
+
class Console:
|
|
42
|
+
"""
|
|
43
|
+
Console URL builder for Flyte resources.
|
|
44
|
+
|
|
45
|
+
Constructs console URLs for various Flyte resources (tasks, runs, apps, triggers)
|
|
46
|
+
based on the configured endpoint and security settings.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
endpoint: The Flyte endpoint (e.g., "dns:///localhost:8090", "https://example.com")
|
|
50
|
+
insecure: Whether to use HTTP (True) or HTTPS (False)
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> console = Console("dns:///example.com", insecure=False)
|
|
54
|
+
>>> url = console.task_url(project="myproject", domain="development", task_name="mytask")
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, endpoint: str, insecure: bool = False):
|
|
58
|
+
"""
|
|
59
|
+
Initialize Console with endpoint and security configuration.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
endpoint: The Flyte endpoint URL
|
|
63
|
+
insecure: Whether to use HTTP (True) or HTTPS (False)
|
|
64
|
+
"""
|
|
65
|
+
self._endpoint = endpoint
|
|
66
|
+
self._insecure = insecure
|
|
67
|
+
self._http_domain = self._compute_http_domain()
|
|
68
|
+
|
|
69
|
+
def _compute_http_domain(self) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Compute the HTTP domain from the endpoint.
|
|
72
|
+
|
|
73
|
+
Internal method that extracts and normalizes the domain from various
|
|
74
|
+
endpoint formats (dns://, http://, https://).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The normalized HTTP(S) domain URL
|
|
78
|
+
"""
|
|
79
|
+
scheme = "http" if self._insecure else "https"
|
|
80
|
+
parsed = urlparse(self._endpoint)
|
|
81
|
+
if parsed.scheme == "dns":
|
|
82
|
+
domain = parsed.path.lstrip("/")
|
|
83
|
+
else:
|
|
84
|
+
domain = parsed.netloc or parsed.path
|
|
85
|
+
|
|
86
|
+
# TODO: make console url configurable
|
|
87
|
+
domain_split = domain.split(":")
|
|
88
|
+
if domain_split[0] == "localhost":
|
|
89
|
+
# Always use port 8080 for localhost, until the to do is done.
|
|
90
|
+
domain = "localhost:8080"
|
|
91
|
+
|
|
92
|
+
return f"{scheme}://{domain}"
|
|
93
|
+
|
|
94
|
+
def _resource_url(self, project: str, domain: str, resource: str, resource_name: str) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Internal helper to build a resource URL.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
project: Project name
|
|
100
|
+
domain: Domain name
|
|
101
|
+
resource: Resource type (e.g., "tasks", "runs", "apps", "triggers")
|
|
102
|
+
resource_name: Resource identifier
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The full console URL for the resource
|
|
106
|
+
"""
|
|
107
|
+
return f"{self._http_domain}/v2/domain/{domain}/project/{project}/{resource}/{resource_name}"
|
|
108
|
+
|
|
109
|
+
def run_url(self, project: str, domain: str, run_name: str) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Build console URL for a run.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
project: Project name
|
|
115
|
+
domain: Domain name
|
|
116
|
+
run_name: Run identifier
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Console URL for the run
|
|
120
|
+
"""
|
|
121
|
+
return self._resource_url(project, domain, "runs", run_name)
|
|
122
|
+
|
|
123
|
+
def app_url(self, project: str, domain: str, app_name: str) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Build console URL for an app.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
project: Project name
|
|
129
|
+
domain: Domain name
|
|
130
|
+
app_name: App identifier
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Console URL for the app
|
|
134
|
+
"""
|
|
135
|
+
return self._resource_url(project, domain, "apps", app_name)
|
|
136
|
+
|
|
137
|
+
def task_url(self, project: str, domain: str, task_name: str) -> str:
|
|
138
|
+
"""
|
|
139
|
+
Build console URL for a task.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
project: Project name
|
|
143
|
+
domain: Domain name
|
|
144
|
+
task_name: Task identifier
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Console URL for the task
|
|
148
|
+
"""
|
|
149
|
+
return self._resource_url(project, domain, "tasks", task_name)
|
|
150
|
+
|
|
151
|
+
def trigger_url(self, project: str, domain: str, task_name: str, trigger_name: str) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Build console URL for a trigger.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
project: Project name
|
|
157
|
+
domain: Domain name
|
|
158
|
+
task_name: Task identifier
|
|
159
|
+
trigger_name: Trigger identifier
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Console URL for the trigger
|
|
163
|
+
"""
|
|
164
|
+
return self._resource_url(project, domain, "triggers", f"{task_name}/{trigger_name}")
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def endpoint(self) -> str:
|
|
168
|
+
"""The configured endpoint."""
|
|
169
|
+
return self._endpoint
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def insecure(self) -> bool:
|
|
173
|
+
"""Whether insecure (HTTP) mode is enabled."""
|
|
174
|
+
return self._insecure
|
|
175
|
+
|
|
176
|
+
|
|
21
177
|
class ClientSet:
|
|
22
178
|
def __init__(
|
|
23
179
|
self,
|
|
24
180
|
channel: grpc.aio.Channel,
|
|
25
181
|
endpoint: str,
|
|
26
182
|
insecure: bool = False,
|
|
27
|
-
data_proxy_channel: grpc.aio.Channel | None = None,
|
|
28
183
|
**kwargs,
|
|
29
184
|
):
|
|
30
185
|
self.endpoint = endpoint
|
|
31
186
|
self.insecure = insecure
|
|
32
187
|
self._channel = channel
|
|
188
|
+
self._console = Console(self.endpoint, self.insecure)
|
|
33
189
|
self._admin_client = admin_pb2_grpc.AdminServiceStub(channel=channel)
|
|
34
190
|
self._task_service = task_service_pb2_grpc.TaskServiceStub(channel=channel)
|
|
191
|
+
self._app_service = app_service_pb2_grpc.AppServiceStub(channel=channel)
|
|
35
192
|
self._run_service = run_service_pb2_grpc.RunServiceStub(channel=channel)
|
|
36
193
|
self._dataproxy = dataproxy_pb2_grpc.DataProxyServiceStub(channel=channel)
|
|
37
194
|
self._log_service = run_logs_service_pb2_grpc.RunLogsServiceStub(channel=channel)
|
|
38
195
|
self._secrets_service = secret_pb2_grpc.SecretServiceStub(channel=channel)
|
|
196
|
+
self._identity_service = identity_pb2_grpc.IdentityServiceStub(channel=channel)
|
|
197
|
+
self._trigger_service = trigger_service_pb2_grpc.TriggerServiceStub(channel=channel)
|
|
39
198
|
|
|
40
199
|
@classmethod
|
|
41
200
|
async def for_endpoint(cls, endpoint: str, *, insecure: bool = False, **kwargs) -> ClientSet:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
del kwargs["headless"]
|
|
46
|
-
del kwargs["command"]
|
|
47
|
-
del kwargs["client_id"]
|
|
48
|
-
del kwargs["client_credentials_secret"]
|
|
49
|
-
del kwargs["client_config"]
|
|
50
|
-
del kwargs["rpc_retries"]
|
|
51
|
-
del kwargs["http_proxy_url"]
|
|
52
|
-
return cls(await create_channel(endpoint, insecure=insecure, **kwargs), endpoint, insecure=insecure, **kwargs)
|
|
201
|
+
return cls(
|
|
202
|
+
await create_channel(endpoint, None, insecure=insecure, **kwargs), endpoint, insecure=insecure, **kwargs
|
|
203
|
+
)
|
|
53
204
|
|
|
54
205
|
@classmethod
|
|
55
|
-
async def for_api_key(cls, api_key: str, **kwargs) -> ClientSet:
|
|
56
|
-
|
|
206
|
+
async def for_api_key(cls, api_key: str, *, insecure: bool = False, **kwargs) -> ClientSet:
|
|
207
|
+
from flyte.remote._client.auth._auth_utils import decode_api_key
|
|
208
|
+
|
|
209
|
+
# Parsing the API key is done in create_channel, but cleaner to redo it here rather than getting create_channel
|
|
210
|
+
# to return the endpoint
|
|
211
|
+
endpoint, _, _, _ = decode_api_key(api_key)
|
|
212
|
+
|
|
213
|
+
return cls(
|
|
214
|
+
await create_channel(None, api_key, insecure=insecure, **kwargs), endpoint, insecure=insecure, **kwargs
|
|
215
|
+
)
|
|
57
216
|
|
|
58
217
|
@classmethod
|
|
59
218
|
async def for_serverless(cls) -> ClientSet:
|
|
@@ -75,6 +234,10 @@ class ClientSet:
|
|
|
75
234
|
def task_service(self) -> TaskService:
|
|
76
235
|
return self._task_service
|
|
77
236
|
|
|
237
|
+
@property
|
|
238
|
+
def app_service(self) -> AppService:
|
|
239
|
+
return self._app_service
|
|
240
|
+
|
|
78
241
|
@property
|
|
79
242
|
def run_service(self) -> RunService:
|
|
80
243
|
return self._run_service
|
|
@@ -91,5 +254,30 @@ class ClientSet:
|
|
|
91
254
|
def secrets_service(self) -> SecretService:
|
|
92
255
|
return self._secrets_service
|
|
93
256
|
|
|
257
|
+
@property
|
|
258
|
+
def identity_service(self) -> IdentityService:
|
|
259
|
+
return self._identity_service
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def trigger_service(self) -> TriggerService:
|
|
263
|
+
return self._trigger_service
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def console(self) -> Console:
|
|
267
|
+
"""
|
|
268
|
+
Get the Console instance for this client.
|
|
269
|
+
|
|
270
|
+
Returns a Console configured with this client's endpoint and security settings.
|
|
271
|
+
Use this to build console URLs for Flyte resources.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Console instance
|
|
275
|
+
|
|
276
|
+
Example:
|
|
277
|
+
>>> client = get_client()
|
|
278
|
+
>>> url = client.console.task_url(project="myproj", domain="dev", task_name="mytask")
|
|
279
|
+
"""
|
|
280
|
+
return self._console
|
|
281
|
+
|
|
94
282
|
async def close(self, grace: float | None = None):
|
|
95
283
|
return await self._channel.close(grace=grace)
|
flyte/remote/_common.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Literal, Tuple
|
|
3
|
+
|
|
4
|
+
from flyteidl2.common import list_pb2
|
|
5
|
+
from google.protobuf.json_format import MessageToDict, MessageToJson
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToJSONMixin:
|
|
9
|
+
"""
|
|
10
|
+
A mixin class that provides a method to convert an object to a JSON-serializable dictionary.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def to_dict(self) -> dict:
|
|
14
|
+
"""
|
|
15
|
+
Convert the object to a JSON-serializable dictionary.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
dict: A dictionary representation of the object.
|
|
19
|
+
"""
|
|
20
|
+
if hasattr(self, "pb2"):
|
|
21
|
+
return MessageToDict(self.pb2)
|
|
22
|
+
else:
|
|
23
|
+
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
|
24
|
+
|
|
25
|
+
def to_json(self) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Convert the object to a JSON string.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
str: A JSON string representation of the object.
|
|
31
|
+
"""
|
|
32
|
+
return MessageToJson(self.pb2) if hasattr(self, "pb2") else json.dumps(self.to_dict())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def sorting(sort_by: Tuple[str, Literal["asc", "desc"]] | None = None) -> list_pb2.Sort:
|
|
36
|
+
"""
|
|
37
|
+
Create a protobuf Sort object from a sorting tuple.
|
|
38
|
+
|
|
39
|
+
:param sort_by: Tuple of (field_name, direction) for sorting, defaults to ("created_at", "asc").
|
|
40
|
+
:return: A protobuf Sort object.
|
|
41
|
+
"""
|
|
42
|
+
sort_by = sort_by or ("created_at", "asc")
|
|
43
|
+
return list_pb2.Sort(
|
|
44
|
+
key=sort_by[0],
|
|
45
|
+
direction=(list_pb2.Sort.ASCENDING if sort_by[1] == "asc" else list_pb2.Sort.DESCENDING),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def filtering(created_by_subject: str | None = None, *filters: list_pb2.Filter) -> list[list_pb2.Filter]:
|
|
50
|
+
"""
|
|
51
|
+
Create a list of filter objects, optionally including a filter by creator subject.
|
|
52
|
+
|
|
53
|
+
:param created_by_subject: Optional subject to filter by creator.
|
|
54
|
+
:param filters: Additional filters to include.
|
|
55
|
+
:return: A list of protobuf Filter objects.
|
|
56
|
+
"""
|
|
57
|
+
filter_list = list(filters) if filters else []
|
|
58
|
+
if created_by_subject:
|
|
59
|
+
filter_list.append(
|
|
60
|
+
list_pb2.Filter(
|
|
61
|
+
function=list_pb2.Filter.Function.EQUAL,
|
|
62
|
+
field="created_by",
|
|
63
|
+
values=[created_by_subject],
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
return filter_list
|