flyte 0.1.0__py3-none-any.whl → 0.2.0a0__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.

Potentially problematic release.


This version of flyte might be problematic. Click here for more details.

Files changed (219) hide show
  1. flyte/__init__.py +78 -2
  2. flyte/_bin/__init__.py +0 -0
  3. flyte/_bin/runtime.py +152 -0
  4. flyte/_build.py +26 -0
  5. flyte/_cache/__init__.py +12 -0
  6. flyte/_cache/cache.py +145 -0
  7. flyte/_cache/defaults.py +9 -0
  8. flyte/_cache/policy_function_body.py +42 -0
  9. flyte/_code_bundle/__init__.py +8 -0
  10. flyte/_code_bundle/_ignore.py +113 -0
  11. flyte/_code_bundle/_packaging.py +187 -0
  12. flyte/_code_bundle/_utils.py +323 -0
  13. flyte/_code_bundle/bundle.py +209 -0
  14. flyte/_context.py +152 -0
  15. flyte/_deploy.py +243 -0
  16. flyte/_doc.py +29 -0
  17. flyte/_docstring.py +32 -0
  18. flyte/_environment.py +84 -0
  19. flyte/_excepthook.py +37 -0
  20. flyte/_group.py +32 -0
  21. flyte/_hash.py +23 -0
  22. flyte/_image.py +762 -0
  23. flyte/_initialize.py +492 -0
  24. flyte/_interface.py +84 -0
  25. flyte/_internal/__init__.py +3 -0
  26. flyte/_internal/controllers/__init__.py +128 -0
  27. flyte/_internal/controllers/_local_controller.py +193 -0
  28. flyte/_internal/controllers/_trace.py +41 -0
  29. flyte/_internal/controllers/remote/__init__.py +60 -0
  30. flyte/_internal/controllers/remote/_action.py +146 -0
  31. flyte/_internal/controllers/remote/_client.py +47 -0
  32. flyte/_internal/controllers/remote/_controller.py +494 -0
  33. flyte/_internal/controllers/remote/_core.py +410 -0
  34. flyte/_internal/controllers/remote/_informer.py +361 -0
  35. flyte/_internal/controllers/remote/_service_protocol.py +50 -0
  36. flyte/_internal/imagebuild/__init__.py +11 -0
  37. flyte/_internal/imagebuild/docker_builder.py +427 -0
  38. flyte/_internal/imagebuild/image_builder.py +246 -0
  39. flyte/_internal/imagebuild/remote_builder.py +0 -0
  40. flyte/_internal/resolvers/__init__.py +0 -0
  41. flyte/_internal/resolvers/_task_module.py +54 -0
  42. flyte/_internal/resolvers/common.py +31 -0
  43. flyte/_internal/resolvers/default.py +28 -0
  44. flyte/_internal/runtime/__init__.py +0 -0
  45. flyte/_internal/runtime/convert.py +342 -0
  46. flyte/_internal/runtime/entrypoints.py +135 -0
  47. flyte/_internal/runtime/io.py +136 -0
  48. flyte/_internal/runtime/resources_serde.py +138 -0
  49. flyte/_internal/runtime/task_serde.py +330 -0
  50. flyte/_internal/runtime/taskrunner.py +191 -0
  51. flyte/_internal/runtime/types_serde.py +54 -0
  52. flyte/_logging.py +135 -0
  53. flyte/_map.py +215 -0
  54. flyte/_pod.py +19 -0
  55. flyte/_protos/__init__.py +0 -0
  56. flyte/_protos/common/authorization_pb2.py +66 -0
  57. flyte/_protos/common/authorization_pb2.pyi +108 -0
  58. flyte/_protos/common/authorization_pb2_grpc.py +4 -0
  59. flyte/_protos/common/identifier_pb2.py +71 -0
  60. flyte/_protos/common/identifier_pb2.pyi +82 -0
  61. flyte/_protos/common/identifier_pb2_grpc.py +4 -0
  62. flyte/_protos/common/identity_pb2.py +48 -0
  63. flyte/_protos/common/identity_pb2.pyi +72 -0
  64. flyte/_protos/common/identity_pb2_grpc.py +4 -0
  65. flyte/_protos/common/list_pb2.py +36 -0
  66. flyte/_protos/common/list_pb2.pyi +71 -0
  67. flyte/_protos/common/list_pb2_grpc.py +4 -0
  68. flyte/_protos/common/policy_pb2.py +37 -0
  69. flyte/_protos/common/policy_pb2.pyi +27 -0
  70. flyte/_protos/common/policy_pb2_grpc.py +4 -0
  71. flyte/_protos/common/role_pb2.py +37 -0
  72. flyte/_protos/common/role_pb2.pyi +53 -0
  73. flyte/_protos/common/role_pb2_grpc.py +4 -0
  74. flyte/_protos/common/runtime_version_pb2.py +28 -0
  75. flyte/_protos/common/runtime_version_pb2.pyi +24 -0
  76. flyte/_protos/common/runtime_version_pb2_grpc.py +4 -0
  77. flyte/_protos/logs/dataplane/payload_pb2.py +100 -0
  78. flyte/_protos/logs/dataplane/payload_pb2.pyi +177 -0
  79. flyte/_protos/logs/dataplane/payload_pb2_grpc.py +4 -0
  80. flyte/_protos/secret/definition_pb2.py +49 -0
  81. flyte/_protos/secret/definition_pb2.pyi +93 -0
  82. flyte/_protos/secret/definition_pb2_grpc.py +4 -0
  83. flyte/_protos/secret/payload_pb2.py +62 -0
  84. flyte/_protos/secret/payload_pb2.pyi +94 -0
  85. flyte/_protos/secret/payload_pb2_grpc.py +4 -0
  86. flyte/_protos/secret/secret_pb2.py +38 -0
  87. flyte/_protos/secret/secret_pb2.pyi +6 -0
  88. flyte/_protos/secret/secret_pb2_grpc.py +198 -0
  89. flyte/_protos/secret/secret_pb2_grpc_grpc.py +198 -0
  90. flyte/_protos/validate/validate/validate_pb2.py +76 -0
  91. flyte/_protos/workflow/common_pb2.py +27 -0
  92. flyte/_protos/workflow/common_pb2.pyi +14 -0
  93. flyte/_protos/workflow/common_pb2_grpc.py +4 -0
  94. flyte/_protos/workflow/environment_pb2.py +29 -0
  95. flyte/_protos/workflow/environment_pb2.pyi +12 -0
  96. flyte/_protos/workflow/environment_pb2_grpc.py +4 -0
  97. flyte/_protos/workflow/node_execution_service_pb2.py +26 -0
  98. flyte/_protos/workflow/node_execution_service_pb2.pyi +4 -0
  99. flyte/_protos/workflow/node_execution_service_pb2_grpc.py +32 -0
  100. flyte/_protos/workflow/queue_service_pb2.py +105 -0
  101. flyte/_protos/workflow/queue_service_pb2.pyi +146 -0
  102. flyte/_protos/workflow/queue_service_pb2_grpc.py +172 -0
  103. flyte/_protos/workflow/run_definition_pb2.py +128 -0
  104. flyte/_protos/workflow/run_definition_pb2.pyi +314 -0
  105. flyte/_protos/workflow/run_definition_pb2_grpc.py +4 -0
  106. flyte/_protos/workflow/run_logs_service_pb2.py +41 -0
  107. flyte/_protos/workflow/run_logs_service_pb2.pyi +28 -0
  108. flyte/_protos/workflow/run_logs_service_pb2_grpc.py +69 -0
  109. flyte/_protos/workflow/run_service_pb2.py +129 -0
  110. flyte/_protos/workflow/run_service_pb2.pyi +171 -0
  111. flyte/_protos/workflow/run_service_pb2_grpc.py +412 -0
  112. flyte/_protos/workflow/state_service_pb2.py +66 -0
  113. flyte/_protos/workflow/state_service_pb2.pyi +75 -0
  114. flyte/_protos/workflow/state_service_pb2_grpc.py +138 -0
  115. flyte/_protos/workflow/task_definition_pb2.py +79 -0
  116. flyte/_protos/workflow/task_definition_pb2.pyi +81 -0
  117. flyte/_protos/workflow/task_definition_pb2_grpc.py +4 -0
  118. flyte/_protos/workflow/task_service_pb2.py +60 -0
  119. flyte/_protos/workflow/task_service_pb2.pyi +59 -0
  120. flyte/_protos/workflow/task_service_pb2_grpc.py +138 -0
  121. flyte/_resources.py +226 -0
  122. flyte/_retry.py +32 -0
  123. flyte/_reusable_environment.py +25 -0
  124. flyte/_run.py +482 -0
  125. flyte/_secret.py +61 -0
  126. flyte/_task.py +449 -0
  127. flyte/_task_environment.py +183 -0
  128. flyte/_timeout.py +47 -0
  129. flyte/_tools.py +27 -0
  130. flyte/_trace.py +120 -0
  131. flyte/_utils/__init__.py +26 -0
  132. flyte/_utils/asyn.py +119 -0
  133. flyte/_utils/async_cache.py +139 -0
  134. flyte/_utils/coro_management.py +23 -0
  135. flyte/_utils/file_handling.py +72 -0
  136. flyte/_utils/helpers.py +134 -0
  137. flyte/_utils/lazy_module.py +54 -0
  138. flyte/_utils/org_discovery.py +57 -0
  139. flyte/_utils/uv_script_parser.py +49 -0
  140. flyte/_version.py +21 -0
  141. flyte/cli/__init__.py +3 -0
  142. flyte/cli/_abort.py +28 -0
  143. flyte/cli/_common.py +337 -0
  144. flyte/cli/_create.py +145 -0
  145. flyte/cli/_delete.py +23 -0
  146. flyte/cli/_deploy.py +152 -0
  147. flyte/cli/_gen.py +163 -0
  148. flyte/cli/_get.py +310 -0
  149. flyte/cli/_params.py +538 -0
  150. flyte/cli/_run.py +231 -0
  151. flyte/cli/main.py +166 -0
  152. flyte/config/__init__.py +3 -0
  153. flyte/config/_config.py +216 -0
  154. flyte/config/_internal.py +64 -0
  155. flyte/config/_reader.py +207 -0
  156. flyte/connectors/__init__.py +0 -0
  157. flyte/errors.py +172 -0
  158. flyte/extras/__init__.py +5 -0
  159. flyte/extras/_container.py +263 -0
  160. flyte/io/__init__.py +27 -0
  161. flyte/io/_dir.py +448 -0
  162. flyte/io/_file.py +467 -0
  163. flyte/io/_structured_dataset/__init__.py +129 -0
  164. flyte/io/_structured_dataset/basic_dfs.py +219 -0
  165. flyte/io/_structured_dataset/structured_dataset.py +1061 -0
  166. flyte/models.py +391 -0
  167. flyte/remote/__init__.py +26 -0
  168. flyte/remote/_client/__init__.py +0 -0
  169. flyte/remote/_client/_protocols.py +133 -0
  170. flyte/remote/_client/auth/__init__.py +12 -0
  171. flyte/remote/_client/auth/_auth_utils.py +14 -0
  172. flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
  173. flyte/remote/_client/auth/_authenticators/base.py +397 -0
  174. flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
  175. flyte/remote/_client/auth/_authenticators/device_code.py +118 -0
  176. flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
  177. flyte/remote/_client/auth/_authenticators/factory.py +200 -0
  178. flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
  179. flyte/remote/_client/auth/_channel.py +215 -0
  180. flyte/remote/_client/auth/_client_config.py +83 -0
  181. flyte/remote/_client/auth/_default_html.py +32 -0
  182. flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
  183. flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
  184. flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
  185. flyte/remote/_client/auth/_keyring.py +143 -0
  186. flyte/remote/_client/auth/_token_client.py +260 -0
  187. flyte/remote/_client/auth/errors.py +16 -0
  188. flyte/remote/_client/controlplane.py +95 -0
  189. flyte/remote/_console.py +18 -0
  190. flyte/remote/_data.py +159 -0
  191. flyte/remote/_logs.py +176 -0
  192. flyte/remote/_project.py +85 -0
  193. flyte/remote/_run.py +970 -0
  194. flyte/remote/_secret.py +132 -0
  195. flyte/remote/_task.py +391 -0
  196. flyte/report/__init__.py +3 -0
  197. flyte/report/_report.py +178 -0
  198. flyte/report/_template.html +124 -0
  199. flyte/storage/__init__.py +29 -0
  200. flyte/storage/_config.py +233 -0
  201. flyte/storage/_remote_fs.py +34 -0
  202. flyte/storage/_storage.py +271 -0
  203. flyte/storage/_utils.py +5 -0
  204. flyte/syncify/__init__.py +56 -0
  205. flyte/syncify/_api.py +371 -0
  206. flyte/types/__init__.py +36 -0
  207. flyte/types/_interface.py +40 -0
  208. flyte/types/_pickle.py +118 -0
  209. flyte/types/_renderer.py +162 -0
  210. flyte/types/_string_literals.py +120 -0
  211. flyte/types/_type_engine.py +2287 -0
  212. flyte/types/_utils.py +80 -0
  213. flyte-0.2.0a0.dist-info/METADATA +249 -0
  214. flyte-0.2.0a0.dist-info/RECORD +218 -0
  215. {flyte-0.1.0.dist-info → flyte-0.2.0a0.dist-info}/WHEEL +2 -1
  216. flyte-0.2.0a0.dist-info/entry_points.txt +3 -0
  217. flyte-0.2.0a0.dist-info/top_level.txt +1 -0
  218. flyte-0.1.0.dist-info/METADATA +0 -6
  219. flyte-0.1.0.dist-info/RECORD +0 -5
flyte/remote/_data.py ADDED
@@ -0,0 +1,159 @@
1
+ import asyncio
2
+ import hashlib
3
+ import os
4
+ import typing
5
+ import uuid
6
+ from base64 import b64encode
7
+ from datetime import timedelta
8
+ from functools import lru_cache
9
+ from pathlib import Path
10
+ from typing import Tuple
11
+
12
+ import aiofiles
13
+ import grpc
14
+ import httpx
15
+ from flyteidl.service import dataproxy_pb2
16
+ from google.protobuf import duration_pb2
17
+
18
+ from flyte._initialize import CommonInit, ensure_client, get_client, get_common_config
19
+ from flyte._logging import make_hyperlink
20
+ from flyte.errors import InitializationError, RuntimeSystemError
21
+
22
+ _UPLOAD_EXPIRES_IN = timedelta(seconds=60)
23
+
24
+
25
+ def get_extra_headers_for_protocol(native_url: str) -> typing.Dict[str, str]:
26
+ """
27
+ For Azure Blob Storage, we need to set certain headers for http request.
28
+ This is used when we work with signed urls.
29
+ :param native_url:
30
+ :return:
31
+ """
32
+ if native_url.startswith("abfs://"):
33
+ return {"x-ms-blob-type": "BlockBlob"}
34
+ return {}
35
+
36
+
37
+ @lru_cache
38
+ def hash_file(file_path: typing.Union[os.PathLike, str]) -> Tuple[bytes, str, int]:
39
+ """
40
+ Hash a file and produce a digest to be used as a version
41
+ """
42
+ h = hashlib.md5()
43
+ size = 0
44
+
45
+ with open(file_path, "rb") as file:
46
+ while True:
47
+ # Reading is buffered, so we can read smaller chunks.
48
+ chunk = file.read(h.block_size)
49
+ if not chunk:
50
+ break
51
+ h.update(chunk)
52
+ size += len(chunk)
53
+
54
+ return h.digest(), h.hexdigest(), size
55
+
56
+
57
+ async def _upload_single_file(
58
+ cfg: CommonInit, fp: Path, verify: bool = True, basedir: str | None = None
59
+ ) -> Tuple[str, str]:
60
+ md5_bytes, str_digest, _ = hash_file(fp)
61
+ from flyte._logging import logger
62
+
63
+ try:
64
+ expires_in_pb = duration_pb2.Duration()
65
+ expires_in_pb.FromTimedelta(_UPLOAD_EXPIRES_IN)
66
+ client = get_client()
67
+ resp = await client.dataproxy_service.CreateUploadLocation( # type: ignore
68
+ dataproxy_pb2.CreateUploadLocationRequest(
69
+ project=cfg.project,
70
+ domain=cfg.domain,
71
+ content_md5=md5_bytes,
72
+ filename=fp.name,
73
+ expires_in=expires_in_pb,
74
+ filename_root=basedir,
75
+ add_content_md5_metadata=True,
76
+ )
77
+ )
78
+ except grpc.aio.AioRpcError as e:
79
+ if e.code() == grpc.StatusCode.NOT_FOUND:
80
+ raise RuntimeSystemError(
81
+ "NotFound", f"Failed to get signed url for {fp}, please check your project and domain."
82
+ )
83
+ elif e.code() == grpc.StatusCode.PERMISSION_DENIED:
84
+ raise RuntimeSystemError(
85
+ "PermissionDenied", f"Failed to get signed url for {fp}, please check your permissions."
86
+ )
87
+ elif e.code() == grpc.StatusCode.UNAVAILABLE:
88
+ raise InitializationError("EndpointUnavailable", "user", "Service is unavailable.")
89
+ else:
90
+ raise RuntimeSystemError(e.code().value, f"Failed to get signed url for {fp}.")
91
+ except Exception as e:
92
+ raise RuntimeSystemError(type(e).__name__, f"Failed to get signed url for {fp}.") from e
93
+ logger.debug(f'Uploading to {make_hyperlink("signed url", resp.signed_url)} for {fp}')
94
+ extra_headers = get_extra_headers_for_protocol(resp.native_url)
95
+ extra_headers.update(resp.headers)
96
+ encoded_md5 = b64encode(md5_bytes)
97
+ content_length = fp.stat().st_size
98
+
99
+ async with aiofiles.open(str(fp), "rb") as file:
100
+ extra_headers.update({"Content-Length": str(content_length), "Content-MD5": encoded_md5.decode("utf-8")})
101
+ async with httpx.AsyncClient(verify=verify) as aclient:
102
+ put_resp = await aclient.put(resp.signed_url, headers=extra_headers, content=file)
103
+ if put_resp.status_code != 200:
104
+ raise RuntimeSystemError(
105
+ "UploadFailed",
106
+ f"Failed to upload {fp} to {resp.signed_url}, status code: {put_resp.status_code}, "
107
+ f"response: {put_resp.text}",
108
+ )
109
+ # TODO in old code we did this
110
+ # if self._config.platform.insecure_skip_verify is True
111
+ # else self._config.platform.ca_cert_file_path,
112
+ logger.debug(f"Uploaded with digest {str_digest}, blob location is {resp.native_url}")
113
+ return str_digest, resp.native_url
114
+
115
+
116
+ async def upload_file(fp: Path, verify: bool = True) -> Tuple[str, str]:
117
+ """
118
+ Uploads a file to a remote location and returns the remote URI.
119
+
120
+ :param fp: The file path to upload.
121
+ :param verify: Whether to verify the certificate for HTTPS requests.
122
+ :return: A tuple containing the MD5 digest and the remote URI.
123
+ """
124
+ # This is a placeholder implementation. Replace with actual upload logic.
125
+ ensure_client()
126
+ cfg = get_common_config()
127
+ if not fp.is_file():
128
+ raise ValueError(f"{fp} is not a single file, upload arg must be a single file.")
129
+ return await _upload_single_file(cfg, fp, verify=verify)
130
+
131
+
132
+ async def upload_dir(dir_path: Path, verify: bool = True) -> str:
133
+ """
134
+ Uploads a directory to a remote location and returns the remote URI.
135
+
136
+ :param dir_path: The directory path to upload.
137
+ :param verify: Whether to verify the certificate for HTTPS requests.
138
+ :return: The remote URI of the uploaded directory.
139
+ """
140
+ # This is a placeholder implementation. Replace with actual upload logic.
141
+ ensure_client()
142
+ cfg = get_common_config()
143
+ if not dir_path.is_dir():
144
+ raise ValueError(f"{dir_path} is not a directory, upload arg must be a directory.")
145
+
146
+ prefix = uuid.uuid4().hex
147
+
148
+ files = dir_path.rglob("*")
149
+ uploaded_files = []
150
+ for file in files:
151
+ if file.is_file():
152
+ uploaded_files.append(_upload_single_file(cfg, file, verify=verify, basedir=prefix))
153
+
154
+ urls = await asyncio.gather(*uploaded_files)
155
+ native_url = urls[0][1] # Assuming all files are uploaded to the same prefix
156
+ # native_url is of the form s3://my-s3-bucket/flytesnacks/development/{prefix}/source/empty.md
157
+ uri = native_url.split(prefix)[0] + "/" + prefix
158
+
159
+ return uri
flyte/remote/_logs.py ADDED
@@ -0,0 +1,176 @@
1
+ import asyncio
2
+ from collections import deque
3
+ from dataclasses import dataclass
4
+ from typing import AsyncGenerator, AsyncIterator
5
+
6
+ import grpc
7
+ from rich.console import Console
8
+ from rich.live import Live
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+
12
+ from flyte._initialize import ensure_client, get_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.errors import LogsNotYetAvailableError
16
+ from flyte.syncify import syncify
17
+
18
+ style_map = {
19
+ payload_pb2.LogLineOriginator.SYSTEM: "bold magenta",
20
+ payload_pb2.LogLineOriginator.USER: "cyan",
21
+ payload_pb2.LogLineOriginator.UNKNOWN: "light red",
22
+ }
23
+
24
+
25
+ def _format_line(logline: payload_pb2.LogLine, show_ts: bool, filter_system: bool) -> Text | None:
26
+ if filter_system:
27
+ if logline.originator == payload_pb2.LogLineOriginator.SYSTEM:
28
+ return None
29
+ style = style_map.get(logline.originator, "")
30
+ if "flyte" in logline.message and "flyte.errors" not in logline.message:
31
+ if filter_system:
32
+ return None
33
+ style = "dim"
34
+ ts = ""
35
+ if show_ts:
36
+ ts = f"[{logline.timestamp.ToDatetime().isoformat()}]"
37
+ return Text(f"{ts} {logline.message}", style=style)
38
+
39
+
40
+ class AsyncLogViewer:
41
+ """
42
+ A class to view logs asynchronously in the console or terminal or jupyter notebook.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ log_source: AsyncIterator,
48
+ max_lines: int = 30,
49
+ name: str = "Logs",
50
+ show_ts: bool = False,
51
+ filter_system: bool = False,
52
+ panel: bool = False,
53
+ ):
54
+ self.console = Console()
55
+ self.log_source = log_source
56
+ self.max_lines = max_lines
57
+ self.lines: deque = deque(maxlen=max_lines + 1)
58
+ self.name = name
59
+ self.show_ts = show_ts
60
+ self.total_lines = 0
61
+ self.filter_flyte = filter_system
62
+ self.panel = panel
63
+
64
+ def _render(self) -> Panel | Text:
65
+ log_text = Text()
66
+ for line in self.lines:
67
+ log_text.append(line)
68
+ if self.panel:
69
+ return Panel(log_text, title=self.name, border_style="yellow")
70
+ return log_text
71
+
72
+ async def run(self):
73
+ with Live(self._render(), refresh_per_second=20, console=self.console) as live:
74
+ try:
75
+ async for logline in self.log_source:
76
+ formatted = _format_line(logline, show_ts=self.show_ts, filter_system=self.filter_flyte)
77
+ if formatted:
78
+ self.lines.append(formatted)
79
+ self.total_lines += 1
80
+ live.update(self._render())
81
+ except asyncio.CancelledError:
82
+ pass
83
+ except KeyboardInterrupt:
84
+ pass
85
+ except StopAsyncIteration:
86
+ self.console.print("[dim]Log stream ended.[/dim]")
87
+ except LogsNotYetAvailableError as e:
88
+ self.console.print(f"[red]Error:[/red] {e}")
89
+ live.update("")
90
+ self.console.print(f"Scrolled {self.total_lines} lines of logs.")
91
+
92
+
93
+ @dataclass
94
+ class Logs:
95
+ @syncify
96
+ @classmethod
97
+ async def tail(
98
+ cls,
99
+ action_id: run_definition_pb2.ActionIdentifier,
100
+ attempt: int = 1,
101
+ retry: int = 3,
102
+ ) -> AsyncGenerator[payload_pb2.LogLine, None]:
103
+ """
104
+ Tail the logs for a given action ID and attempt.
105
+ :param action_id: The action ID to tail logs for.
106
+ :param attempt: The attempt number (default is 0).
107
+ """
108
+ ensure_client()
109
+ retries = 0
110
+ while True:
111
+ try:
112
+ resp = get_client().logs_service.TailLogs(
113
+ run_logs_service_pb2.TailLogsRequest(action_id=action_id, attempt=attempt)
114
+ )
115
+ async for log_set in resp:
116
+ if log_set.logs:
117
+ for log in log_set.logs:
118
+ for line in log.lines:
119
+ yield line
120
+ return
121
+ except asyncio.CancelledError:
122
+ return
123
+ except KeyboardInterrupt:
124
+ return
125
+ except StopAsyncIteration:
126
+ return
127
+ except grpc.aio.AioRpcError as e:
128
+ retries += 1
129
+ if retries >= retry:
130
+ if e.code() == grpc.StatusCode.NOT_FOUND:
131
+ raise LogsNotYetAvailableError(
132
+ f"Log stream not available for action {action_id.name} in run {action_id.run.name}."
133
+ )
134
+ else:
135
+ await asyncio.sleep(1)
136
+
137
+ @classmethod
138
+ async def create_viewer(
139
+ cls,
140
+ action_id: run_definition_pb2.ActionIdentifier,
141
+ attempt: int = 1,
142
+ max_lines: int = 30,
143
+ show_ts: bool = False,
144
+ raw: bool = False,
145
+ filter_system: bool = False,
146
+ panel: bool = False,
147
+ ):
148
+ """
149
+ Create a log viewer for a given action ID and attempt.
150
+ :param action_id: Action ID to view logs for.
151
+ :param attempt: Attempt number (default is 1).
152
+ :param max_lines: Maximum number of lines to show if using the viewer. The logger will scroll
153
+ and keep only max_lines in view.
154
+ :param show_ts: Whether to show timestamps in the logs.
155
+ :param raw: if True, return the raw log lines instead of a viewer.
156
+ :param filter_system: Whether to filter log lines based on system logs.
157
+ :param panel: Whether to use a panel for the log viewer. only applicable if raw is False.
158
+ """
159
+ if attempt < 1:
160
+ raise ValueError("Attempt number must be greater than 0.")
161
+ if raw:
162
+ console = Console()
163
+ async for line in cls.tail.aio(action_id=action_id, attempt=attempt):
164
+ line_text = _format_line(line, show_ts=show_ts, filter_system=filter_system)
165
+ if line_text:
166
+ console.print(line_text, end="")
167
+ return
168
+ viewer = AsyncLogViewer(
169
+ log_source=cls.tail.aio(action_id=action_id, attempt=attempt),
170
+ max_lines=max_lines,
171
+ show_ts=show_ts,
172
+ name=f"{action_id.run.name}:{action_id.name} ({attempt})",
173
+ filter_system=filter_system,
174
+ panel=panel,
175
+ )
176
+ await viewer.run()
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import AsyncIterator, Iterator, Literal, Tuple, Union
5
+
6
+ import rich.repr
7
+ from flyteidl.admin import common_pb2, project_pb2
8
+
9
+ from flyte._initialize import ensure_client, get_client, get_common_config
10
+ from flyte.syncify import syncify
11
+
12
+
13
+ @dataclass
14
+ class Project:
15
+ """
16
+ A class representing a project in the Union API.
17
+ """
18
+
19
+ _pb2: project_pb2.Project
20
+
21
+ @syncify
22
+ @classmethod
23
+ async def get(cls, name: str, org: str | None = None) -> Project:
24
+ """
25
+ Get a run by its ID or name. If both are provided, the ID will take precedence.
26
+
27
+ :param name: The name of the project.
28
+ :param org: The organization of the project (if applicable).
29
+ """
30
+ ensure_client()
31
+ service = get_client().project_domain_service # type: ignore
32
+ resp = await service.GetProject(
33
+ project_pb2.ProjectGetRequest(
34
+ id=name,
35
+ org=org,
36
+ )
37
+ )
38
+ return cls(resp)
39
+
40
+ @syncify
41
+ @classmethod
42
+ async def listall(
43
+ cls,
44
+ filters: str | None = None,
45
+ sort_by: Tuple[str, Literal["asc", "desc"]] | None = None,
46
+ ) -> Union[AsyncIterator[Project], Iterator[Project]]:
47
+ """
48
+ Get a run by its ID or name. If both are provided, the ID will take precedence.
49
+
50
+ :param filters: The filters to apply to the project list.
51
+ :param sort_by: The sorting criteria for the project list, in the format (field, order).
52
+ :return: An iterator of projects.
53
+ """
54
+ ensure_client()
55
+ token = None
56
+ sort_by = sort_by or ("created_at", "asc")
57
+ sort_pb2 = common_pb2.Sort(
58
+ key=sort_by[0], direction=common_pb2.Sort.ASCENDING if sort_by[1] == "asc" else common_pb2.Sort.DESCENDING
59
+ )
60
+ org = get_common_config().org
61
+ while True:
62
+ resp = await get_client().project_domain_service.ListProjects( # type: ignore
63
+ project_pb2.ProjectListRequest(
64
+ limit=100,
65
+ token=token,
66
+ filters=filters,
67
+ sort_by=sort_pb2,
68
+ org=org,
69
+ )
70
+ )
71
+ token = resp.token
72
+ for p in resp.projects:
73
+ yield cls(p)
74
+ if not token:
75
+ break
76
+
77
+ def __rich_repr__(self) -> rich.repr.Result:
78
+ yield "name", self._pb2.name
79
+ yield "id", self._pb2.id
80
+ yield "description", self._pb2.description
81
+ yield "state", project_pb2.Project.ProjectState.Name(self._pb2.state)
82
+ yield (
83
+ "labels",
84
+ ", ".join([f"{k}: {v}" for k, v in self._pb2.labels.values.items()]) if self._pb2.labels else None,
85
+ )