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.
Files changed (266) hide show
  1. flyte/__init__.py +83 -30
  2. flyte/_bin/connect.py +61 -0
  3. flyte/_bin/debug.py +38 -0
  4. flyte/_bin/runtime.py +87 -19
  5. flyte/_bin/serve.py +351 -0
  6. flyte/_build.py +3 -2
  7. flyte/_cache/cache.py +6 -5
  8. flyte/_cache/local_cache.py +216 -0
  9. flyte/_code_bundle/_ignore.py +31 -5
  10. flyte/_code_bundle/_packaging.py +42 -11
  11. flyte/_code_bundle/_utils.py +57 -34
  12. flyte/_code_bundle/bundle.py +130 -27
  13. flyte/_constants.py +1 -0
  14. flyte/_context.py +21 -5
  15. flyte/_custom_context.py +73 -0
  16. flyte/_debug/constants.py +37 -0
  17. flyte/_debug/utils.py +17 -0
  18. flyte/_debug/vscode.py +315 -0
  19. flyte/_deploy.py +396 -75
  20. flyte/_deployer.py +109 -0
  21. flyte/_environment.py +94 -11
  22. flyte/_excepthook.py +37 -0
  23. flyte/_group.py +2 -1
  24. flyte/_hash.py +1 -16
  25. flyte/_image.py +544 -231
  26. flyte/_initialize.py +456 -316
  27. flyte/_interface.py +40 -5
  28. flyte/_internal/controllers/__init__.py +22 -8
  29. flyte/_internal/controllers/_local_controller.py +159 -35
  30. flyte/_internal/controllers/_trace.py +18 -10
  31. flyte/_internal/controllers/remote/__init__.py +38 -9
  32. flyte/_internal/controllers/remote/_action.py +82 -12
  33. flyte/_internal/controllers/remote/_client.py +6 -2
  34. flyte/_internal/controllers/remote/_controller.py +290 -64
  35. flyte/_internal/controllers/remote/_core.py +155 -95
  36. flyte/_internal/controllers/remote/_informer.py +40 -20
  37. flyte/_internal/controllers/remote/_service_protocol.py +2 -2
  38. flyte/_internal/imagebuild/__init__.py +2 -10
  39. flyte/_internal/imagebuild/docker_builder.py +391 -84
  40. flyte/_internal/imagebuild/image_builder.py +111 -55
  41. flyte/_internal/imagebuild/remote_builder.py +409 -0
  42. flyte/_internal/imagebuild/utils.py +79 -0
  43. flyte/_internal/resolvers/_app_env_module.py +92 -0
  44. flyte/_internal/resolvers/_task_module.py +5 -38
  45. flyte/_internal/resolvers/app_env.py +26 -0
  46. flyte/_internal/resolvers/common.py +8 -1
  47. flyte/_internal/resolvers/default.py +2 -2
  48. flyte/_internal/runtime/convert.py +319 -36
  49. flyte/_internal/runtime/entrypoints.py +106 -18
  50. flyte/_internal/runtime/io.py +71 -23
  51. flyte/_internal/runtime/resources_serde.py +21 -7
  52. flyte/_internal/runtime/reuse.py +125 -0
  53. flyte/_internal/runtime/rusty.py +196 -0
  54. flyte/_internal/runtime/task_serde.py +239 -66
  55. flyte/_internal/runtime/taskrunner.py +48 -8
  56. flyte/_internal/runtime/trigger_serde.py +162 -0
  57. flyte/_internal/runtime/types_serde.py +7 -16
  58. flyte/_keyring/file.py +115 -0
  59. flyte/_link.py +30 -0
  60. flyte/_logging.py +241 -42
  61. flyte/_map.py +312 -0
  62. flyte/_metrics.py +59 -0
  63. flyte/_module.py +74 -0
  64. flyte/_pod.py +30 -0
  65. flyte/_resources.py +296 -33
  66. flyte/_retry.py +1 -7
  67. flyte/_reusable_environment.py +72 -7
  68. flyte/_run.py +462 -132
  69. flyte/_secret.py +47 -11
  70. flyte/_serve.py +333 -0
  71. flyte/_task.py +245 -56
  72. flyte/_task_environment.py +219 -97
  73. flyte/_task_plugins.py +47 -0
  74. flyte/_tools.py +8 -8
  75. flyte/_trace.py +15 -24
  76. flyte/_trigger.py +1027 -0
  77. flyte/_utils/__init__.py +12 -1
  78. flyte/_utils/asyn.py +3 -1
  79. flyte/_utils/async_cache.py +139 -0
  80. flyte/_utils/coro_management.py +5 -4
  81. flyte/_utils/description_parser.py +19 -0
  82. flyte/_utils/docker_credentials.py +173 -0
  83. flyte/_utils/helpers.py +45 -19
  84. flyte/_utils/module_loader.py +123 -0
  85. flyte/_utils/org_discovery.py +57 -0
  86. flyte/_utils/uv_script_parser.py +8 -1
  87. flyte/_version.py +16 -3
  88. flyte/app/__init__.py +27 -0
  89. flyte/app/_app_environment.py +362 -0
  90. flyte/app/_connector_environment.py +40 -0
  91. flyte/app/_deploy.py +130 -0
  92. flyte/app/_parameter.py +343 -0
  93. flyte/app/_runtime/__init__.py +3 -0
  94. flyte/app/_runtime/app_serde.py +383 -0
  95. flyte/app/_types.py +113 -0
  96. flyte/app/extras/__init__.py +9 -0
  97. flyte/app/extras/_auth_middleware.py +217 -0
  98. flyte/app/extras/_fastapi.py +93 -0
  99. flyte/app/extras/_model_loader/__init__.py +3 -0
  100. flyte/app/extras/_model_loader/config.py +7 -0
  101. flyte/app/extras/_model_loader/loader.py +288 -0
  102. flyte/cli/__init__.py +12 -0
  103. flyte/cli/_abort.py +28 -0
  104. flyte/cli/_build.py +114 -0
  105. flyte/cli/_common.py +493 -0
  106. flyte/cli/_create.py +371 -0
  107. flyte/cli/_delete.py +45 -0
  108. flyte/cli/_deploy.py +401 -0
  109. flyte/cli/_gen.py +316 -0
  110. flyte/cli/_get.py +446 -0
  111. flyte/cli/_option.py +33 -0
  112. flyte/{_cli → cli}/_params.py +57 -17
  113. flyte/cli/_plugins.py +209 -0
  114. flyte/cli/_prefetch.py +292 -0
  115. flyte/cli/_run.py +690 -0
  116. flyte/cli/_serve.py +338 -0
  117. flyte/cli/_update.py +86 -0
  118. flyte/cli/_user.py +20 -0
  119. flyte/cli/main.py +246 -0
  120. flyte/config/__init__.py +2 -167
  121. flyte/config/_config.py +215 -163
  122. flyte/config/_internal.py +10 -1
  123. flyte/config/_reader.py +225 -0
  124. flyte/connectors/__init__.py +11 -0
  125. flyte/connectors/_connector.py +330 -0
  126. flyte/connectors/_server.py +194 -0
  127. flyte/connectors/utils.py +159 -0
  128. flyte/errors.py +134 -2
  129. flyte/extend.py +24 -0
  130. flyte/extras/_container.py +69 -56
  131. flyte/git/__init__.py +3 -0
  132. flyte/git/_config.py +279 -0
  133. flyte/io/__init__.py +8 -1
  134. flyte/io/{structured_dataset → _dataframe}/__init__.py +32 -30
  135. flyte/io/{structured_dataset → _dataframe}/basic_dfs.py +75 -68
  136. flyte/io/{structured_dataset/structured_dataset.py → _dataframe/dataframe.py} +207 -242
  137. flyte/io/_dir.py +575 -113
  138. flyte/io/_file.py +587 -141
  139. flyte/io/_hashing_io.py +342 -0
  140. flyte/io/extend.py +7 -0
  141. flyte/models.py +635 -0
  142. flyte/prefetch/__init__.py +22 -0
  143. flyte/prefetch/_hf_model.py +563 -0
  144. flyte/remote/__init__.py +14 -3
  145. flyte/remote/_action.py +879 -0
  146. flyte/remote/_app.py +346 -0
  147. flyte/remote/_auth_metadata.py +42 -0
  148. flyte/remote/_client/_protocols.py +62 -4
  149. flyte/remote/_client/auth/_auth_utils.py +19 -0
  150. flyte/remote/_client/auth/_authenticators/base.py +8 -2
  151. flyte/remote/_client/auth/_authenticators/device_code.py +4 -5
  152. flyte/remote/_client/auth/_authenticators/factory.py +4 -0
  153. flyte/remote/_client/auth/_authenticators/passthrough.py +79 -0
  154. flyte/remote/_client/auth/_authenticators/pkce.py +17 -18
  155. flyte/remote/_client/auth/_channel.py +47 -18
  156. flyte/remote/_client/auth/_client_config.py +5 -3
  157. flyte/remote/_client/auth/_keyring.py +15 -2
  158. flyte/remote/_client/auth/_token_client.py +3 -3
  159. flyte/remote/_client/controlplane.py +206 -18
  160. flyte/remote/_common.py +66 -0
  161. flyte/remote/_data.py +107 -22
  162. flyte/remote/_logs.py +116 -33
  163. flyte/remote/_project.py +21 -19
  164. flyte/remote/_run.py +164 -631
  165. flyte/remote/_secret.py +72 -29
  166. flyte/remote/_task.py +387 -46
  167. flyte/remote/_trigger.py +368 -0
  168. flyte/remote/_user.py +43 -0
  169. flyte/report/_report.py +10 -6
  170. flyte/storage/__init__.py +13 -1
  171. flyte/storage/_config.py +237 -0
  172. flyte/storage/_parallel_reader.py +289 -0
  173. flyte/storage/_storage.py +268 -59
  174. flyte/syncify/__init__.py +56 -0
  175. flyte/syncify/_api.py +414 -0
  176. flyte/types/__init__.py +39 -0
  177. flyte/types/_interface.py +22 -7
  178. flyte/{io/pickle/transformer.py → types/_pickle.py} +37 -9
  179. flyte/types/_string_literals.py +8 -9
  180. flyte/types/_type_engine.py +226 -126
  181. flyte/types/_utils.py +1 -1
  182. flyte-2.0.0b46.data/scripts/debug.py +38 -0
  183. flyte-2.0.0b46.data/scripts/runtime.py +194 -0
  184. flyte-2.0.0b46.dist-info/METADATA +352 -0
  185. flyte-2.0.0b46.dist-info/RECORD +221 -0
  186. flyte-2.0.0b46.dist-info/entry_points.txt +8 -0
  187. flyte-2.0.0b46.dist-info/licenses/LICENSE +201 -0
  188. flyte/_api_commons.py +0 -3
  189. flyte/_cli/_common.py +0 -299
  190. flyte/_cli/_create.py +0 -42
  191. flyte/_cli/_delete.py +0 -23
  192. flyte/_cli/_deploy.py +0 -140
  193. flyte/_cli/_get.py +0 -235
  194. flyte/_cli/_run.py +0 -174
  195. flyte/_cli/main.py +0 -98
  196. flyte/_datastructures.py +0 -342
  197. flyte/_internal/controllers/pbhash.py +0 -39
  198. flyte/_protos/common/authorization_pb2.py +0 -66
  199. flyte/_protos/common/authorization_pb2.pyi +0 -108
  200. flyte/_protos/common/authorization_pb2_grpc.py +0 -4
  201. flyte/_protos/common/identifier_pb2.py +0 -71
  202. flyte/_protos/common/identifier_pb2.pyi +0 -82
  203. flyte/_protos/common/identifier_pb2_grpc.py +0 -4
  204. flyte/_protos/common/identity_pb2.py +0 -48
  205. flyte/_protos/common/identity_pb2.pyi +0 -72
  206. flyte/_protos/common/identity_pb2_grpc.py +0 -4
  207. flyte/_protos/common/list_pb2.py +0 -36
  208. flyte/_protos/common/list_pb2.pyi +0 -69
  209. flyte/_protos/common/list_pb2_grpc.py +0 -4
  210. flyte/_protos/common/policy_pb2.py +0 -37
  211. flyte/_protos/common/policy_pb2.pyi +0 -27
  212. flyte/_protos/common/policy_pb2_grpc.py +0 -4
  213. flyte/_protos/common/role_pb2.py +0 -37
  214. flyte/_protos/common/role_pb2.pyi +0 -53
  215. flyte/_protos/common/role_pb2_grpc.py +0 -4
  216. flyte/_protos/common/runtime_version_pb2.py +0 -28
  217. flyte/_protos/common/runtime_version_pb2.pyi +0 -24
  218. flyte/_protos/common/runtime_version_pb2_grpc.py +0 -4
  219. flyte/_protos/logs/dataplane/payload_pb2.py +0 -96
  220. flyte/_protos/logs/dataplane/payload_pb2.pyi +0 -168
  221. flyte/_protos/logs/dataplane/payload_pb2_grpc.py +0 -4
  222. flyte/_protos/secret/definition_pb2.py +0 -49
  223. flyte/_protos/secret/definition_pb2.pyi +0 -93
  224. flyte/_protos/secret/definition_pb2_grpc.py +0 -4
  225. flyte/_protos/secret/payload_pb2.py +0 -62
  226. flyte/_protos/secret/payload_pb2.pyi +0 -94
  227. flyte/_protos/secret/payload_pb2_grpc.py +0 -4
  228. flyte/_protos/secret/secret_pb2.py +0 -38
  229. flyte/_protos/secret/secret_pb2.pyi +0 -6
  230. flyte/_protos/secret/secret_pb2_grpc.py +0 -198
  231. flyte/_protos/secret/secret_pb2_grpc_grpc.py +0 -198
  232. flyte/_protos/validate/validate/validate_pb2.py +0 -76
  233. flyte/_protos/workflow/node_execution_service_pb2.py +0 -26
  234. flyte/_protos/workflow/node_execution_service_pb2.pyi +0 -4
  235. flyte/_protos/workflow/node_execution_service_pb2_grpc.py +0 -32
  236. flyte/_protos/workflow/queue_service_pb2.py +0 -106
  237. flyte/_protos/workflow/queue_service_pb2.pyi +0 -141
  238. flyte/_protos/workflow/queue_service_pb2_grpc.py +0 -172
  239. flyte/_protos/workflow/run_definition_pb2.py +0 -128
  240. flyte/_protos/workflow/run_definition_pb2.pyi +0 -310
  241. flyte/_protos/workflow/run_definition_pb2_grpc.py +0 -4
  242. flyte/_protos/workflow/run_logs_service_pb2.py +0 -41
  243. flyte/_protos/workflow/run_logs_service_pb2.pyi +0 -28
  244. flyte/_protos/workflow/run_logs_service_pb2_grpc.py +0 -69
  245. flyte/_protos/workflow/run_service_pb2.py +0 -133
  246. flyte/_protos/workflow/run_service_pb2.pyi +0 -175
  247. flyte/_protos/workflow/run_service_pb2_grpc.py +0 -412
  248. flyte/_protos/workflow/state_service_pb2.py +0 -58
  249. flyte/_protos/workflow/state_service_pb2.pyi +0 -71
  250. flyte/_protos/workflow/state_service_pb2_grpc.py +0 -138
  251. flyte/_protos/workflow/task_definition_pb2.py +0 -72
  252. flyte/_protos/workflow/task_definition_pb2.pyi +0 -65
  253. flyte/_protos/workflow/task_definition_pb2_grpc.py +0 -4
  254. flyte/_protos/workflow/task_service_pb2.py +0 -44
  255. flyte/_protos/workflow/task_service_pb2.pyi +0 -31
  256. flyte/_protos/workflow/task_service_pb2_grpc.py +0 -104
  257. flyte/io/_dataframe.py +0 -0
  258. flyte/io/pickle/__init__.py +0 -0
  259. flyte/remote/_console.py +0 -18
  260. flyte-0.2.0b1.dist-info/METADATA +0 -179
  261. flyte-0.2.0b1.dist-info/RECORD +0 -204
  262. flyte-0.2.0b1.dist-info/entry_points.txt +0 -3
  263. /flyte/{_cli → _debug}/__init__.py +0 -0
  264. /flyte/{_protos → _keyring}/__init__.py +0 -0
  265. {flyte-0.2.0b1.dist-info → flyte-2.0.0b46.dist-info}/WHEEL +0 -0
  266. {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("Failed to refresh token. Kicking off a full authorization flow.")
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
- async with typing.cast(
367
- typing.AsyncContextManager[Response],
368
- self._http_session.post(
369
- url=self._token_endpoint,
370
- data=data,
371
- headers=self._headers,
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
- return await self._credentials_from_response(resp)
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
- data = await reader.read(1024)
414
- message = data.decode()
415
- headers = message.split("\r\n")
416
- path = headers[0].split(" ")[1]
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 = endpoint.rsplit(":", 1)
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 [{endpoint}], defaulting to 443.")
39
- server_address = (endpoint, 443)
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 in a thread pool
42
- cert = ssl.get_server_certificate(server_address)
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
- if not ssl_credentials:
109
- if insecure_skip_verify:
110
- ssl_credentials = bootstrap_ssl_from_server(endpoint)
111
- elif ca_cert_file_path:
112
- import aiofiles
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
- async with aiofiles.open(ca_cert_file_path, "rb") as f:
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
- unauthenticated_channel = grpc.aio.insecure_channel(endpoint, **kwargs)
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
- return grpc.aio.insecure_channel(endpoint, interceptors=interceptors, **kwargs)
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
- public_client_config = await metadata_service.GetPublicClientConfig(PublicClientAuthConfigRequest())
73
- oauth2_metadata = await metadata_service.GetOAuth2Metadata(OAuth2MetadataRequest())
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
- logger.debug("No access token found in keyring.")
108
- return None
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
- raise AuthenticationError("Token not yet available, try again in some time")
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 grpc
4
- from flyteidl.service import admin_pb2_grpc, dataproxy_pb2_grpc
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
- from flyte._protos.secret import secret_pb2_grpc
7
- from flyte._protos.workflow import run_logs_service_pb2_grpc, run_service_pb2_grpc, task_service_pb2_grpc
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
- if insecure:
43
- del kwargs["api_key"]
44
- del kwargs["auth_type"]
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
- raise NotImplementedError
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)
@@ -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