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
flyte/remote/_data.py CHANGED
@@ -15,8 +15,9 @@ import httpx
15
15
  from flyteidl.service import dataproxy_pb2
16
16
  from google.protobuf import duration_pb2
17
17
 
18
- from flyte._initialize import CommonInit, get_client, get_common_config, requires_client
19
- from flyte.errors import RuntimeSystemError
18
+ from flyte._initialize import CommonInit, ensure_client, get_client, get_init_config, require_project_and_domain
19
+ from flyte.errors import InitializationError, RuntimeSystemError
20
+ from flyte.syncify import syncify
20
21
 
21
22
  _UPLOAD_EXPIRES_IN = timedelta(seconds=60)
22
23
 
@@ -53,9 +54,89 @@ def hash_file(file_path: typing.Union[os.PathLike, str]) -> Tuple[bytes, str, in
53
54
  return h.digest(), h.hexdigest(), size
54
55
 
55
56
 
57
+ async def _upload_with_retry(
58
+ fp: Path,
59
+ signed_url: str,
60
+ extra_headers: dict,
61
+ verify: bool,
62
+ max_retries: int = 3,
63
+ min_backoff_sec: float = 0.5,
64
+ max_backoff_sec: float = 10.0,
65
+ ):
66
+ """
67
+ Upload file to signed URL with exponential backoff retry.
68
+
69
+ Retries on transient network errors and 5xx/429/408 HTTP errors.
70
+ Does not retry on 4xx client errors (except 408/429).
71
+
72
+ Args:
73
+ fp: Path to file to upload
74
+ signed_url: Pre-signed URL for upload
75
+ extra_headers: Headers including Content-MD5, Content-Length
76
+ verify: Whether to verify SSL certificates
77
+ max_retries: Maximum retry attempts (default: 3)
78
+ min_backoff_sec: Initial backoff delay (default: 0.5)
79
+ max_backoff_sec: Maximum backoff delay (default: 10.0)
80
+
81
+ Raises:
82
+ RuntimeSystemError: If upload fails after all retries
83
+ """
84
+ from flyte._logging import logger
85
+
86
+ retry_attempt = 0
87
+ last_error = None
88
+
89
+ while retry_attempt <= max_retries:
90
+ async with aiofiles.open(str(fp), "rb") as file:
91
+ async with httpx.AsyncClient(verify=verify) as aclient:
92
+ put_resp = await aclient.put(signed_url, headers=extra_headers, content=file)
93
+
94
+ # Success
95
+ if put_resp.status_code in [200, 201, 204]:
96
+ if retry_attempt > 0:
97
+ logger.info(f"Upload succeeded after {retry_attempt} retries for {fp.name}")
98
+ return put_resp
99
+
100
+ # Check if retryable status code
101
+ if put_resp.status_code in [408, 429, 500, 502, 503, 504]:
102
+ if retry_attempt >= max_retries:
103
+ raise RuntimeSystemError(
104
+ "UploadFailed",
105
+ f"Failed to upload {fp} after {max_retries} retries: {put_resp.text}",
106
+ )
107
+
108
+ # Backoff and retry
109
+ retry_attempt += 1
110
+ if retry_attempt <= max_retries:
111
+ backoff_delay = min(min_backoff_sec * (2 ** (retry_attempt - 1)), max_backoff_sec)
112
+ logger.warning(
113
+ f"Upload failed for {fp.name}, backing off for {backoff_delay:.2f}s "
114
+ f"[retry {retry_attempt}/{max_retries}]: {last_error}"
115
+ )
116
+ await asyncio.sleep(backoff_delay)
117
+ else:
118
+ # Non-retryable HTTP error
119
+ raise RuntimeSystemError(
120
+ "UploadFailed",
121
+ f"Failed to upload {fp} to {signed_url}, status code: {put_resp.status_code}, "
122
+ f"response: {put_resp.text}",
123
+ )
124
+ return None
125
+
126
+
127
+ @require_project_and_domain
56
128
  async def _upload_single_file(
57
129
  cfg: CommonInit, fp: Path, verify: bool = True, basedir: str | None = None
58
130
  ) -> Tuple[str, str]:
131
+ """
132
+ Upload a single file to remote storage using a signed URL.
133
+
134
+ :param cfg: Configuration containing project and domain information.
135
+ :param fp: Path to the file to upload.
136
+ :param verify: Whether to verify SSL certificates.
137
+ :param basedir: Optional base directory prefix for the remote path.
138
+ :return: Tuple of (MD5 digest hex string, remote native URL).
139
+ """
59
140
  md5_bytes, str_digest, _ = hash_file(fp)
60
141
  from flyte._logging import logger
61
142
 
@@ -77,39 +158,42 @@ async def _upload_single_file(
77
158
  except grpc.aio.AioRpcError as e:
78
159
  if e.code() == grpc.StatusCode.NOT_FOUND:
79
160
  raise RuntimeSystemError(
80
- "NotFound", f"Failed to get signed url for {fp}, please check your project and domain."
161
+ "NotFound", f"Failed to get signed url for {fp}, please check your project and domain: {e.details()}"
81
162
  )
82
163
  elif e.code() == grpc.StatusCode.PERMISSION_DENIED:
83
164
  raise RuntimeSystemError(
84
- "PermissionDenied", f"Failed to get signed url for {fp}, please check your permissions."
165
+ "PermissionDenied", f"Failed to get signed url for {fp}, please check your permissions: {e.details()}"
85
166
  )
167
+ elif e.code() == grpc.StatusCode.UNAVAILABLE:
168
+ raise InitializationError("EndpointUnavailable", "user", "Service is unavailable.")
86
169
  else:
87
- raise RuntimeSystemError(e.code().value, f"Failed to get signed url for {fp}.")
170
+ raise RuntimeSystemError(e.code().value, f"Failed to get signed url for {fp}: {e.details()}")
88
171
  except Exception as e:
89
172
  raise RuntimeSystemError(type(e).__name__, f"Failed to get signed url for {fp}.") from e
90
- logger.debug(f"Uploading to signed url {resp.signed_url} for {fp}")
173
+ logger.debug(f"Uploading to [link={resp.signed_url}]signed url[/link] for [link=file://{fp}]{fp}[/link]")
91
174
  extra_headers = get_extra_headers_for_protocol(resp.native_url)
92
175
  extra_headers.update(resp.headers)
93
176
  encoded_md5 = b64encode(md5_bytes)
94
177
  content_length = fp.stat().st_size
95
178
 
96
- async with aiofiles.open(str(fp), "rb") as file:
97
- extra_headers.update({"Content-Length": str(content_length), "Content-MD5": encoded_md5.decode("utf-8")})
98
- async with httpx.AsyncClient(verify=verify) as aclient:
99
- put_resp = await aclient.put(resp.signed_url, headers=extra_headers, content=file)
100
- if put_resp.status_code != 200:
101
- raise RuntimeSystemError(
102
- "UploadFailed",
103
- f"Failed to upload {fp} to {resp.signed_url}, status code: {put_resp.status_code}",
104
- )
105
- # TODO in old code we did this
106
- # if self._config.platform.insecure_skip_verify is True
107
- # else self._config.platform.ca_cert_file_path,
179
+ # Update headers with MD5 and content length
180
+ extra_headers.update({"Content-Length": str(content_length), "Content-MD5": encoded_md5.decode("utf-8")})
181
+
182
+ await _upload_with_retry(
183
+ fp=fp,
184
+ signed_url=resp.signed_url,
185
+ extra_headers=extra_headers,
186
+ verify=verify,
187
+ max_retries=3,
188
+ min_backoff_sec=0.5,
189
+ max_backoff_sec=10.0,
190
+ )
191
+
108
192
  logger.debug(f"Uploaded with digest {str_digest}, blob location is {resp.native_url}")
109
193
  return str_digest, resp.native_url
110
194
 
111
195
 
112
- @requires_client
196
+ @syncify
113
197
  async def upload_file(fp: Path, verify: bool = True) -> Tuple[str, str]:
114
198
  """
115
199
  Uploads a file to a remote location and returns the remote URI.
@@ -119,13 +203,13 @@ async def upload_file(fp: Path, verify: bool = True) -> Tuple[str, str]:
119
203
  :return: A tuple containing the MD5 digest and the remote URI.
120
204
  """
121
205
  # This is a placeholder implementation. Replace with actual upload logic.
122
- cfg = get_common_config()
206
+ ensure_client()
207
+ cfg = get_init_config()
123
208
  if not fp.is_file():
124
209
  raise ValueError(f"{fp} is not a single file, upload arg must be a single file.")
125
210
  return await _upload_single_file(cfg, fp, verify=verify)
126
211
 
127
212
 
128
- @requires_client
129
213
  async def upload_dir(dir_path: Path, verify: bool = True) -> str:
130
214
  """
131
215
  Uploads a directory to a remote location and returns the remote URI.
@@ -135,7 +219,8 @@ async def upload_dir(dir_path: Path, verify: bool = True) -> str:
135
219
  :return: The remote URI of the uploaded directory.
136
220
  """
137
221
  # This is a placeholder implementation. Replace with actual upload logic.
138
- cfg = get_common_config()
222
+ ensure_client()
223
+ cfg = get_init_config()
139
224
  if not dir_path.is_dir():
140
225
  raise ValueError(f"{dir_path} is not a directory, upload arg must be a directory.")
141
226
 
flyte/remote/_logs.py CHANGED
@@ -3,25 +3,44 @@ from collections import deque
3
3
  from dataclasses import dataclass
4
4
  from typing import AsyncGenerator, AsyncIterator
5
5
 
6
+ import grpc
7
+ from flyteidl2.common import identifier_pb2
8
+ from flyteidl2.logs.dataplane import payload_pb2
9
+ from flyteidl2.workflow import run_logs_service_pb2
6
10
  from rich.console import Console
7
11
  from rich.live import Live
8
12
  from rich.panel import Panel
9
13
  from rich.text import Text
10
14
 
11
- from flyte._api_commons import syncer
12
- from flyte._initialize import get_client, requires_client
13
- from flyte._protos.logs.dataplane import payload_pb2
14
- from flyte._protos.workflow import run_definition_pb2, run_logs_service_pb2
15
+ from flyte._initialize import ensure_client, get_client
16
+ from flyte._logging import logger
17
+ from flyte._tools import ipython_check, ipywidgets_check
18
+ from flyte.errors import LogsNotYetAvailableError
19
+ from flyte.syncify import syncify
15
20
 
21
+ style_map = {
22
+ payload_pb2.LogLineOriginator.SYSTEM: "bold magenta",
23
+ payload_pb2.LogLineOriginator.USER: "cyan",
24
+ payload_pb2.LogLineOriginator.UNKNOWN: "light red",
25
+ }
16
26
 
17
- def _format_line(logline: payload_pb2.LogLine, show_ts: bool) -> Text:
18
- style_map = {
19
- payload_pb2.LogLineOriginator.SYSTEM: "bold magenta",
20
- payload_pb2.LogLineOriginator.USER: "cyan",
21
- payload_pb2.LogLineOriginator.UNKNOWN: "light red",
22
- }
27
+
28
+ def _format_line(logline: payload_pb2.LogLine, show_ts: bool, filter_system: bool) -> Text | None:
29
+ """
30
+ Format a log line for display with optional timestamp and system filtering.
31
+
32
+ :param logline: The log line protobuf to format.
33
+ :param show_ts: Whether to include timestamps.
34
+ :param filter_system: Whether to filter out system log lines.
35
+ :return: A formatted Text object or None if the line should be filtered out.
36
+ """
37
+ if filter_system:
38
+ if logline.originator == payload_pb2.LogLineOriginator.SYSTEM:
39
+ return None
23
40
  style = style_map.get(logline.originator, "")
24
- if "flyte" in logline.message and "flyte.errors" not in logline.message:
41
+ if "[flyte]" in logline.message and "flyte.errors" not in logline.message:
42
+ if filter_system:
43
+ return None
25
44
  style = "dim"
26
45
  ts = ""
27
46
  if show_ts:
@@ -34,7 +53,15 @@ class AsyncLogViewer:
34
53
  A class to view logs asynchronously in the console or terminal or jupyter notebook.
35
54
  """
36
55
 
37
- def __init__(self, log_source: AsyncIterator, max_lines: int = 30, name: str = "Logs", show_ts: bool = False):
56
+ def __init__(
57
+ self,
58
+ log_source: AsyncIterator,
59
+ max_lines: int = 30,
60
+ name: str = "Logs",
61
+ show_ts: bool = False,
62
+ filter_system: bool = False,
63
+ panel: bool = False,
64
+ ):
38
65
  self.console = Console()
39
66
  self.log_source = log_source
40
67
  self.max_lines = max_lines
@@ -42,56 +69,98 @@ class AsyncLogViewer:
42
69
  self.name = name
43
70
  self.show_ts = show_ts
44
71
  self.total_lines = 0
72
+ self.filter_flyte = filter_system
73
+ self.panel = panel
45
74
 
46
- def _render(self):
75
+ def _render(self) -> Panel | Text:
76
+ """
77
+ Render the current log lines as a Panel or Text object for display.
78
+ """
47
79
  log_text = Text()
48
80
  for line in self.lines:
49
81
  log_text.append(line)
50
- return Panel(log_text, title=self.name, border_style="yellow")
82
+ if self.panel:
83
+ return Panel(log_text, title=self.name, border_style="yellow")
84
+ return log_text
51
85
 
52
86
  async def run(self):
53
- with Live(self._render(), refresh_per_second=10, console=self.console) as live:
87
+ """
88
+ Run the log viewer, streaming and displaying log lines until completion.
89
+ """
90
+ with Live(self._render(), refresh_per_second=20, console=self.console) as live:
54
91
  try:
55
92
  async for logline in self.log_source:
56
- formatted = _format_line(logline, show_ts=self.show_ts)
57
- self.lines.append(formatted)
93
+ formatted = _format_line(logline, show_ts=self.show_ts, filter_system=self.filter_flyte)
94
+ if formatted:
95
+ self.lines.append(formatted)
58
96
  self.total_lines += 1
59
97
  live.update(self._render())
60
98
  except asyncio.CancelledError:
61
99
  pass
100
+ except KeyboardInterrupt:
101
+ pass
102
+ except StopAsyncIteration:
103
+ self.console.print("[dim]Log stream ended.[/dim]")
104
+ except LogsNotYetAvailableError as e:
105
+ self.console.print(f"[red]Error:[/red] {e}")
106
+ live.update("")
62
107
  self.console.print(f"Scrolled {self.total_lines} lines of logs.")
63
108
 
64
109
 
65
110
  @dataclass
66
111
  class Logs:
112
+ @syncify
67
113
  @classmethod
68
- @requires_client
69
- @syncer.wrap
70
114
  async def tail(
71
- cls, action_id: run_definition_pb2.ActionIdentifier, attempt: int = 1
115
+ cls,
116
+ action_id: identifier_pb2.ActionIdentifier,
117
+ attempt: int = 1,
118
+ retry: int = 5,
72
119
  ) -> AsyncGenerator[payload_pb2.LogLine, None]:
73
120
  """
74
121
  Tail the logs for a given action ID and attempt.
75
122
  :param action_id: The action ID to tail logs for.
76
123
  :param attempt: The attempt number (default is 0).
77
124
  """
78
- resp = get_client().logs_service.TailLogs(
79
- run_logs_service_pb2.TailLogsRequest(action_id=action_id, attempt=attempt)
80
- )
81
- async for log_set in resp:
82
- if log_set.logs:
83
- for log in log_set.logs:
84
- for line in log.lines:
85
- yield line
125
+ ensure_client()
126
+ retries = 0
127
+ while True:
128
+ try:
129
+ resp = get_client().logs_service.TailLogs(
130
+ run_logs_service_pb2.TailLogsRequest(action_id=action_id, attempt=attempt)
131
+ )
132
+ async for log_set in resp:
133
+ if log_set.logs:
134
+ for log in log_set.logs:
135
+ for line in log.lines:
136
+ yield line
137
+ return
138
+ except asyncio.CancelledError:
139
+ return
140
+ except KeyboardInterrupt:
141
+ return
142
+ except StopAsyncIteration:
143
+ return
144
+ except grpc.aio.AioRpcError as e:
145
+ retries += 1
146
+ if retries >= retry:
147
+ if e.code() == grpc.StatusCode.NOT_FOUND:
148
+ raise LogsNotYetAvailableError(
149
+ f"Log stream not available for action {action_id.name} in run {action_id.run.name}."
150
+ )
151
+ else:
152
+ await asyncio.sleep(2)
86
153
 
87
154
  @classmethod
88
155
  async def create_viewer(
89
156
  cls,
90
- action_id: run_definition_pb2.ActionIdentifier,
157
+ action_id: identifier_pb2.ActionIdentifier,
91
158
  attempt: int = 1,
92
159
  max_lines: int = 30,
93
160
  show_ts: bool = False,
94
161
  raw: bool = False,
162
+ filter_system: bool = False,
163
+ panel: bool = False,
95
164
  ):
96
165
  """
97
166
  Create a log viewer for a given action ID and attempt.
@@ -101,16 +170,30 @@ class Logs:
101
170
  and keep only max_lines in view.
102
171
  :param show_ts: Whether to show timestamps in the logs.
103
172
  :param raw: if True, return the raw log lines instead of a viewer.
173
+ :param filter_system: Whether to filter log lines based on system logs.
174
+ :param panel: Whether to use a panel for the log viewer. only applicable if raw is False.
104
175
  """
176
+ if attempt < 1:
177
+ raise ValueError("Attempt number must be greater than 0.")
178
+
179
+ if ipython_check():
180
+ if not ipywidgets_check():
181
+ logger.warning("IPython widgets is not available, defaulting to console output.")
182
+ raw = True
183
+
105
184
  if raw:
106
185
  console = Console()
107
- async for line in cls.tail.aio(cls, action_id=action_id, attempt=attempt):
108
- console.print(_format_line(line, show_ts=show_ts), end="")
186
+ async for line in cls.tail.aio(action_id=action_id, attempt=attempt):
187
+ line_text = _format_line(line, show_ts=show_ts, filter_system=filter_system)
188
+ if line_text:
189
+ console.print(line_text, end="")
109
190
  return
110
191
  viewer = AsyncLogViewer(
111
- log_source=cls.tail.aio(cls, action_id=action_id, attempt=attempt),
192
+ log_source=cls.tail.aio(action_id=action_id, attempt=attempt),
112
193
  max_lines=max_lines,
113
194
  show_ts=show_ts,
114
- name=f"{action_id.run.name}:{action_id.name}",
195
+ name=f"{action_id.run.name}:{action_id.name} ({attempt})",
196
+ filter_system=filter_system,
197
+ panel=panel,
115
198
  )
116
199
  await viewer.run()
flyte/remote/_project.py CHANGED
@@ -1,27 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
- import typing
4
3
  from dataclasses import dataclass
5
- from typing import AsyncGenerator, Literal, Tuple
4
+ from typing import AsyncIterator, Iterator, Literal, Tuple, Union
6
5
 
7
6
  import rich.repr
8
7
  from flyteidl.admin import common_pb2, project_pb2
9
8
 
10
- from flyte._api_commons import syncer
11
- from flyte._initialize import get_client, get_common_config, requires_client
9
+ from flyte._initialize import ensure_client, get_client
10
+ from flyte.syncify import syncify
12
11
 
12
+ from ._common import ToJSONMixin
13
13
 
14
+
15
+ # TODO Add support for orgs again
14
16
  @dataclass
15
- class Project:
17
+ class Project(ToJSONMixin):
16
18
  """
17
19
  A class representing a project in the Union API.
18
20
  """
19
21
 
20
- _pb2: project_pb2.Project
22
+ pb2: project_pb2.Project
21
23
 
24
+ @syncify
22
25
  @classmethod
23
- @requires_client
24
- @syncer.wrap
25
26
  async def get(cls, name: str, org: str | None = None) -> Project:
26
27
  """
27
28
  Get a run by its ID or name. If both are provided, the ID will take precedence.
@@ -29,23 +30,23 @@ class Project:
29
30
  :param name: The name of the project.
30
31
  :param org: The organization of the project (if applicable).
31
32
  """
33
+ ensure_client()
32
34
  service = get_client().project_domain_service # type: ignore
33
35
  resp = await service.GetProject(
34
36
  project_pb2.ProjectGetRequest(
35
37
  id=name,
36
- org=org,
38
+ # org=org,
37
39
  )
38
40
  )
39
41
  return cls(resp)
40
42
 
43
+ @syncify
41
44
  @classmethod
42
- @requires_client
43
- @syncer.wrap
44
45
  async def listall(
45
46
  cls,
46
47
  filters: str | None = None,
47
48
  sort_by: Tuple[str, Literal["asc", "desc"]] | None = None,
48
- ) -> typing.Union[typing.Iterator[Project], AsyncGenerator[Project, None]]:
49
+ ) -> Union[AsyncIterator[Project], Iterator[Project]]:
49
50
  """
50
51
  Get a run by its ID or name. If both are provided, the ID will take precedence.
51
52
 
@@ -53,12 +54,13 @@ class Project:
53
54
  :param sort_by: The sorting criteria for the project list, in the format (field, order).
54
55
  :return: An iterator of projects.
55
56
  """
57
+ ensure_client()
56
58
  token = None
57
59
  sort_by = sort_by or ("created_at", "asc")
58
60
  sort_pb2 = common_pb2.Sort(
59
61
  key=sort_by[0], direction=common_pb2.Sort.ASCENDING if sort_by[1] == "asc" else common_pb2.Sort.DESCENDING
60
62
  )
61
- org = get_common_config().org
63
+ # org = get_common_config().org
62
64
  while True:
63
65
  resp = await get_client().project_domain_service.ListProjects( # type: ignore
64
66
  project_pb2.ProjectListRequest(
@@ -66,7 +68,7 @@ class Project:
66
68
  token=token,
67
69
  filters=filters,
68
70
  sort_by=sort_pb2,
69
- org=org,
71
+ # org=org,
70
72
  )
71
73
  )
72
74
  token = resp.token
@@ -76,11 +78,11 @@ class Project:
76
78
  break
77
79
 
78
80
  def __rich_repr__(self) -> rich.repr.Result:
79
- yield "name", self._pb2.name
80
- yield "id", self._pb2.id
81
- yield "description", self._pb2.description
82
- yield "state", project_pb2.Project.ProjectState.Name(self._pb2.state)
81
+ yield "name", self.pb2.name
82
+ yield "id", self.pb2.id
83
+ yield "description", self.pb2.description
84
+ yield "state", project_pb2.Project.ProjectState.Name(self.pb2.state)
83
85
  yield (
84
86
  "labels",
85
- ", ".join([f"{k}: {v}" for k, v in self._pb2.labels.values.items()]) if self._pb2.labels else None,
87
+ ", ".join([f"{k}: {v}" for k, v in self.pb2.labels.values.items()]) if self.pb2.labels else None,
86
88
  )