flyte 2.0.0b32__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 (204) hide show
  1. flyte/__init__.py +108 -0
  2. flyte/_bin/__init__.py +0 -0
  3. flyte/_bin/debug.py +38 -0
  4. flyte/_bin/runtime.py +195 -0
  5. flyte/_bin/serve.py +178 -0
  6. flyte/_build.py +26 -0
  7. flyte/_cache/__init__.py +12 -0
  8. flyte/_cache/cache.py +147 -0
  9. flyte/_cache/defaults.py +9 -0
  10. flyte/_cache/local_cache.py +216 -0
  11. flyte/_cache/policy_function_body.py +42 -0
  12. flyte/_code_bundle/__init__.py +8 -0
  13. flyte/_code_bundle/_ignore.py +121 -0
  14. flyte/_code_bundle/_packaging.py +218 -0
  15. flyte/_code_bundle/_utils.py +347 -0
  16. flyte/_code_bundle/bundle.py +266 -0
  17. flyte/_constants.py +1 -0
  18. flyte/_context.py +155 -0
  19. flyte/_custom_context.py +73 -0
  20. flyte/_debug/__init__.py +0 -0
  21. flyte/_debug/constants.py +38 -0
  22. flyte/_debug/utils.py +17 -0
  23. flyte/_debug/vscode.py +307 -0
  24. flyte/_deploy.py +408 -0
  25. flyte/_deployer.py +109 -0
  26. flyte/_doc.py +29 -0
  27. flyte/_docstring.py +32 -0
  28. flyte/_environment.py +122 -0
  29. flyte/_excepthook.py +37 -0
  30. flyte/_group.py +32 -0
  31. flyte/_hash.py +8 -0
  32. flyte/_image.py +1055 -0
  33. flyte/_initialize.py +628 -0
  34. flyte/_interface.py +119 -0
  35. flyte/_internal/__init__.py +3 -0
  36. flyte/_internal/controllers/__init__.py +129 -0
  37. flyte/_internal/controllers/_local_controller.py +239 -0
  38. flyte/_internal/controllers/_trace.py +48 -0
  39. flyte/_internal/controllers/remote/__init__.py +58 -0
  40. flyte/_internal/controllers/remote/_action.py +211 -0
  41. flyte/_internal/controllers/remote/_client.py +47 -0
  42. flyte/_internal/controllers/remote/_controller.py +583 -0
  43. flyte/_internal/controllers/remote/_core.py +465 -0
  44. flyte/_internal/controllers/remote/_informer.py +381 -0
  45. flyte/_internal/controllers/remote/_service_protocol.py +50 -0
  46. flyte/_internal/imagebuild/__init__.py +3 -0
  47. flyte/_internal/imagebuild/docker_builder.py +706 -0
  48. flyte/_internal/imagebuild/image_builder.py +277 -0
  49. flyte/_internal/imagebuild/remote_builder.py +386 -0
  50. flyte/_internal/imagebuild/utils.py +78 -0
  51. flyte/_internal/resolvers/__init__.py +0 -0
  52. flyte/_internal/resolvers/_task_module.py +21 -0
  53. flyte/_internal/resolvers/common.py +31 -0
  54. flyte/_internal/resolvers/default.py +28 -0
  55. flyte/_internal/runtime/__init__.py +0 -0
  56. flyte/_internal/runtime/convert.py +486 -0
  57. flyte/_internal/runtime/entrypoints.py +204 -0
  58. flyte/_internal/runtime/io.py +188 -0
  59. flyte/_internal/runtime/resources_serde.py +152 -0
  60. flyte/_internal/runtime/reuse.py +125 -0
  61. flyte/_internal/runtime/rusty.py +193 -0
  62. flyte/_internal/runtime/task_serde.py +362 -0
  63. flyte/_internal/runtime/taskrunner.py +209 -0
  64. flyte/_internal/runtime/trigger_serde.py +160 -0
  65. flyte/_internal/runtime/types_serde.py +54 -0
  66. flyte/_keyring/__init__.py +0 -0
  67. flyte/_keyring/file.py +115 -0
  68. flyte/_logging.py +300 -0
  69. flyte/_map.py +312 -0
  70. flyte/_module.py +72 -0
  71. flyte/_pod.py +30 -0
  72. flyte/_resources.py +473 -0
  73. flyte/_retry.py +32 -0
  74. flyte/_reusable_environment.py +102 -0
  75. flyte/_run.py +724 -0
  76. flyte/_secret.py +96 -0
  77. flyte/_task.py +550 -0
  78. flyte/_task_environment.py +316 -0
  79. flyte/_task_plugins.py +47 -0
  80. flyte/_timeout.py +47 -0
  81. flyte/_tools.py +27 -0
  82. flyte/_trace.py +119 -0
  83. flyte/_trigger.py +1000 -0
  84. flyte/_utils/__init__.py +30 -0
  85. flyte/_utils/asyn.py +121 -0
  86. flyte/_utils/async_cache.py +139 -0
  87. flyte/_utils/coro_management.py +27 -0
  88. flyte/_utils/docker_credentials.py +173 -0
  89. flyte/_utils/file_handling.py +72 -0
  90. flyte/_utils/helpers.py +134 -0
  91. flyte/_utils/lazy_module.py +54 -0
  92. flyte/_utils/module_loader.py +104 -0
  93. flyte/_utils/org_discovery.py +57 -0
  94. flyte/_utils/uv_script_parser.py +49 -0
  95. flyte/_version.py +34 -0
  96. flyte/app/__init__.py +22 -0
  97. flyte/app/_app_environment.py +157 -0
  98. flyte/app/_deploy.py +125 -0
  99. flyte/app/_input.py +160 -0
  100. flyte/app/_runtime/__init__.py +3 -0
  101. flyte/app/_runtime/app_serde.py +347 -0
  102. flyte/app/_types.py +101 -0
  103. flyte/app/extras/__init__.py +3 -0
  104. flyte/app/extras/_fastapi.py +151 -0
  105. flyte/cli/__init__.py +12 -0
  106. flyte/cli/_abort.py +28 -0
  107. flyte/cli/_build.py +114 -0
  108. flyte/cli/_common.py +468 -0
  109. flyte/cli/_create.py +371 -0
  110. flyte/cli/_delete.py +45 -0
  111. flyte/cli/_deploy.py +293 -0
  112. flyte/cli/_gen.py +176 -0
  113. flyte/cli/_get.py +370 -0
  114. flyte/cli/_option.py +33 -0
  115. flyte/cli/_params.py +554 -0
  116. flyte/cli/_plugins.py +209 -0
  117. flyte/cli/_run.py +597 -0
  118. flyte/cli/_serve.py +64 -0
  119. flyte/cli/_update.py +37 -0
  120. flyte/cli/_user.py +17 -0
  121. flyte/cli/main.py +221 -0
  122. flyte/config/__init__.py +3 -0
  123. flyte/config/_config.py +248 -0
  124. flyte/config/_internal.py +73 -0
  125. flyte/config/_reader.py +225 -0
  126. flyte/connectors/__init__.py +11 -0
  127. flyte/connectors/_connector.py +270 -0
  128. flyte/connectors/_server.py +197 -0
  129. flyte/connectors/utils.py +135 -0
  130. flyte/errors.py +243 -0
  131. flyte/extend.py +19 -0
  132. flyte/extras/__init__.py +5 -0
  133. flyte/extras/_container.py +286 -0
  134. flyte/git/__init__.py +3 -0
  135. flyte/git/_config.py +21 -0
  136. flyte/io/__init__.py +29 -0
  137. flyte/io/_dataframe/__init__.py +131 -0
  138. flyte/io/_dataframe/basic_dfs.py +223 -0
  139. flyte/io/_dataframe/dataframe.py +1026 -0
  140. flyte/io/_dir.py +910 -0
  141. flyte/io/_file.py +914 -0
  142. flyte/io/_hashing_io.py +342 -0
  143. flyte/models.py +479 -0
  144. flyte/py.typed +0 -0
  145. flyte/remote/__init__.py +35 -0
  146. flyte/remote/_action.py +738 -0
  147. flyte/remote/_app.py +57 -0
  148. flyte/remote/_client/__init__.py +0 -0
  149. flyte/remote/_client/_protocols.py +189 -0
  150. flyte/remote/_client/auth/__init__.py +12 -0
  151. flyte/remote/_client/auth/_auth_utils.py +14 -0
  152. flyte/remote/_client/auth/_authenticators/__init__.py +0 -0
  153. flyte/remote/_client/auth/_authenticators/base.py +403 -0
  154. flyte/remote/_client/auth/_authenticators/client_credentials.py +73 -0
  155. flyte/remote/_client/auth/_authenticators/device_code.py +117 -0
  156. flyte/remote/_client/auth/_authenticators/external_command.py +79 -0
  157. flyte/remote/_client/auth/_authenticators/factory.py +200 -0
  158. flyte/remote/_client/auth/_authenticators/pkce.py +516 -0
  159. flyte/remote/_client/auth/_channel.py +213 -0
  160. flyte/remote/_client/auth/_client_config.py +85 -0
  161. flyte/remote/_client/auth/_default_html.py +32 -0
  162. flyte/remote/_client/auth/_grpc_utils/__init__.py +0 -0
  163. flyte/remote/_client/auth/_grpc_utils/auth_interceptor.py +288 -0
  164. flyte/remote/_client/auth/_grpc_utils/default_metadata_interceptor.py +151 -0
  165. flyte/remote/_client/auth/_keyring.py +152 -0
  166. flyte/remote/_client/auth/_token_client.py +260 -0
  167. flyte/remote/_client/auth/errors.py +16 -0
  168. flyte/remote/_client/controlplane.py +128 -0
  169. flyte/remote/_common.py +30 -0
  170. flyte/remote/_console.py +19 -0
  171. flyte/remote/_data.py +161 -0
  172. flyte/remote/_logs.py +185 -0
  173. flyte/remote/_project.py +88 -0
  174. flyte/remote/_run.py +386 -0
  175. flyte/remote/_secret.py +142 -0
  176. flyte/remote/_task.py +527 -0
  177. flyte/remote/_trigger.py +306 -0
  178. flyte/remote/_user.py +33 -0
  179. flyte/report/__init__.py +3 -0
  180. flyte/report/_report.py +182 -0
  181. flyte/report/_template.html +124 -0
  182. flyte/storage/__init__.py +36 -0
  183. flyte/storage/_config.py +237 -0
  184. flyte/storage/_parallel_reader.py +274 -0
  185. flyte/storage/_remote_fs.py +34 -0
  186. flyte/storage/_storage.py +456 -0
  187. flyte/storage/_utils.py +5 -0
  188. flyte/syncify/__init__.py +56 -0
  189. flyte/syncify/_api.py +375 -0
  190. flyte/types/__init__.py +52 -0
  191. flyte/types/_interface.py +40 -0
  192. flyte/types/_pickle.py +145 -0
  193. flyte/types/_renderer.py +162 -0
  194. flyte/types/_string_literals.py +119 -0
  195. flyte/types/_type_engine.py +2254 -0
  196. flyte/types/_utils.py +80 -0
  197. flyte-2.0.0b32.data/scripts/debug.py +38 -0
  198. flyte-2.0.0b32.data/scripts/runtime.py +195 -0
  199. flyte-2.0.0b32.dist-info/METADATA +351 -0
  200. flyte-2.0.0b32.dist-info/RECORD +204 -0
  201. flyte-2.0.0b32.dist-info/WHEEL +5 -0
  202. flyte-2.0.0b32.dist-info/entry_points.txt +7 -0
  203. flyte-2.0.0b32.dist-info/licenses/LICENSE +201 -0
  204. flyte-2.0.0b32.dist-info/top_level.txt +1 -0
@@ -0,0 +1,306 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from functools import cached_property
5
+ from typing import AsyncIterator
6
+
7
+ import grpc.aio
8
+ from flyteidl2.common import identifier_pb2, list_pb2
9
+ from flyteidl2.task import common_pb2, task_definition_pb2
10
+ from flyteidl2.trigger import trigger_definition_pb2, trigger_service_pb2
11
+
12
+ import flyte
13
+ from flyte._initialize import ensure_client, get_client, get_init_config
14
+ from flyte._internal.runtime import trigger_serde
15
+ from flyte.syncify import syncify
16
+
17
+ from ._common import ToJSONMixin
18
+ from ._task import Task, TaskDetails
19
+
20
+
21
+ @dataclass
22
+ class TriggerDetails(ToJSONMixin):
23
+ pb2: trigger_definition_pb2.TriggerDetails
24
+
25
+ @syncify
26
+ @classmethod
27
+ async def get(cls, *, name: str, task_name: str) -> TriggerDetails:
28
+ """
29
+ Retrieve detailed information about a specific trigger by its name.
30
+ """
31
+ ensure_client()
32
+ cfg = get_init_config()
33
+ resp = await get_client().trigger_service.GetTriggerDetails(
34
+ request=trigger_service_pb2.GetTriggerDetailsRequest(
35
+ name=identifier_pb2.TriggerName(
36
+ task_name=task_name,
37
+ name=name,
38
+ org=cfg.org,
39
+ project=cfg.project,
40
+ domain=cfg.domain,
41
+ ),
42
+ )
43
+ )
44
+ return cls(pb2=resp.trigger)
45
+
46
+ @property
47
+ def name(self) -> str:
48
+ return self.id.name.name
49
+
50
+ @property
51
+ def id(self) -> identifier_pb2.TriggerIdentifier:
52
+ return self.pb2.id
53
+
54
+ @property
55
+ def task_name(self) -> str:
56
+ return self.pb2.id.name.task_name
57
+
58
+ @property
59
+ def automation_spec(self) -> common_pb2.TriggerAutomationSpec:
60
+ return self.pb2.automation_spec
61
+
62
+ @property
63
+ def metadata(self) -> trigger_definition_pb2.TriggerMetadata:
64
+ return self.pb2.metadata
65
+
66
+ @property
67
+ def status(self) -> trigger_definition_pb2.TriggerStatus:
68
+ return self.pb2.status
69
+
70
+ @property
71
+ def is_active(self) -> bool:
72
+ return self.pb2.spec.active
73
+
74
+ @cached_property
75
+ def trigger(self) -> trigger_definition_pb2.Trigger:
76
+ return trigger_definition_pb2.Trigger(
77
+ id=self.pb2.id,
78
+ automation_spec=self.automation_spec,
79
+ metadata=self.metadata,
80
+ status=self.status,
81
+ active=self.is_active,
82
+ )
83
+
84
+
85
+ @dataclass
86
+ class Trigger(ToJSONMixin):
87
+ pb2: trigger_definition_pb2.Trigger
88
+ details: TriggerDetails | None = None
89
+
90
+ @syncify
91
+ @classmethod
92
+ async def create(
93
+ cls,
94
+ trigger: flyte.Trigger,
95
+ task_name: str,
96
+ task_version: str | None = None,
97
+ ) -> Trigger:
98
+ """
99
+ Create a new trigger in the Flyte platform.
100
+
101
+ :param trigger: The flyte.Trigger object containing the trigger definition.
102
+ :param task_name: Optional name of the task to associate with the trigger.
103
+ """
104
+ ensure_client()
105
+ cfg = get_init_config()
106
+
107
+ # Fetch the task to ensure it exists and to get its input definitions
108
+ try:
109
+ lazy = (
110
+ Task.get(name=task_name, version=task_version)
111
+ if task_version
112
+ else Task.get(name=task_name, auto_version="latest")
113
+ )
114
+ task: TaskDetails = await lazy.fetch.aio()
115
+
116
+ task_trigger = await trigger_serde.to_task_trigger(
117
+ t=trigger,
118
+ task_name=task_name,
119
+ task_inputs=task.pb2.spec.task_template.interface.inputs,
120
+ task_default_inputs=list(task.pb2.spec.default_inputs),
121
+ )
122
+
123
+ resp = await get_client().trigger_service.DeployTrigger(
124
+ request=trigger_service_pb2.DeployTriggerRequest(
125
+ name=identifier_pb2.TriggerName(
126
+ name=trigger.name,
127
+ task_name=task_name,
128
+ org=cfg.org,
129
+ project=cfg.project,
130
+ domain=cfg.domain,
131
+ ),
132
+ spec=trigger_definition_pb2.TriggerSpec(
133
+ active=task_trigger.spec.active,
134
+ inputs=task_trigger.spec.inputs,
135
+ run_spec=task_trigger.spec.run_spec,
136
+ task_version=task.version,
137
+ ),
138
+ automation_spec=task_trigger.automation_spec,
139
+ )
140
+ )
141
+
142
+ details = TriggerDetails(pb2=resp.trigger)
143
+
144
+ return cls(pb2=details.trigger, details=details)
145
+ except grpc.aio.AioRpcError as e:
146
+ if e.code() == grpc.StatusCode.NOT_FOUND:
147
+ raise ValueError(f"Task {task_name}:{task_version or 'latest'} not found") from e
148
+ raise
149
+
150
+ @syncify
151
+ @classmethod
152
+ async def get(cls, *, name: str, task_name: str) -> TriggerDetails:
153
+ """
154
+ Retrieve a trigger by its name and associated task name.
155
+ """
156
+ return await TriggerDetails.get.aio(name=name, task_name=task_name)
157
+
158
+ @syncify
159
+ @classmethod
160
+ async def listall(
161
+ cls, task_name: str | None = None, task_version: str | None = None, limit: int = 100
162
+ ) -> AsyncIterator[Trigger]:
163
+ """
164
+ List all triggers associated with a specific task or all tasks if no task name is provided.
165
+ """
166
+ ensure_client()
167
+ cfg = get_init_config()
168
+ token = None
169
+ task_name_id = None
170
+ project_id = None
171
+ task_id = None
172
+ if task_name and task_version:
173
+ task_id = task_definition_pb2.TaskIdentifier(
174
+ name=task_name,
175
+ project=cfg.project,
176
+ domain=cfg.domain,
177
+ org=cfg.org,
178
+ version=task_version,
179
+ )
180
+ elif task_name:
181
+ task_name_id = task_definition_pb2.TaskName(
182
+ name=task_name,
183
+ project=cfg.project,
184
+ domain=cfg.domain,
185
+ org=cfg.org,
186
+ )
187
+ else:
188
+ project_id = identifier_pb2.ProjectIdentifier(
189
+ organization=cfg.org,
190
+ domain=cfg.domain,
191
+ name=cfg.project,
192
+ )
193
+
194
+ while True:
195
+ resp = await get_client().trigger_service.ListTriggers(
196
+ request=trigger_service_pb2.ListTriggersRequest(
197
+ project_id=project_id,
198
+ task_id=task_id,
199
+ task_name=task_name_id,
200
+ request=list_pb2.ListRequest(
201
+ limit=limit,
202
+ token=token,
203
+ ),
204
+ )
205
+ )
206
+ token = resp.token
207
+ for r in resp.triggers:
208
+ yield cls(r)
209
+ if not token:
210
+ break
211
+
212
+ @syncify
213
+ @classmethod
214
+ async def update(cls, name: str, task_name: str, active: bool):
215
+ """
216
+ Pause a trigger by its name and associated task name.
217
+ """
218
+ ensure_client()
219
+ cfg = get_init_config()
220
+ await get_client().trigger_service.UpdateTriggers(
221
+ request=trigger_service_pb2.UpdateTriggersRequest(
222
+ names=[
223
+ identifier_pb2.TriggerName(
224
+ org=cfg.org,
225
+ project=cfg.project,
226
+ domain=cfg.domain,
227
+ name=name,
228
+ task_name=task_name,
229
+ )
230
+ ],
231
+ active=active,
232
+ )
233
+ )
234
+
235
+ @syncify
236
+ @classmethod
237
+ async def delete(cls, name: str, task_name: str):
238
+ """
239
+ Delete a trigger by its name.
240
+ """
241
+ ensure_client()
242
+ cfg = get_init_config()
243
+ await get_client().trigger_service.DeleteTriggers(
244
+ request=trigger_service_pb2.DeleteTriggersRequest(
245
+ names=[
246
+ identifier_pb2.TriggerName(
247
+ org=cfg.org,
248
+ project=cfg.project,
249
+ domain=cfg.domain,
250
+ name=name,
251
+ task_name=task_name,
252
+ )
253
+ ],
254
+ )
255
+ )
256
+
257
+ @property
258
+ def id(self) -> identifier_pb2.TriggerIdentifier:
259
+ return self.pb2.id
260
+
261
+ @property
262
+ def name(self) -> str:
263
+ return self.id.name.name
264
+
265
+ @property
266
+ def task_name(self) -> str:
267
+ return self.id.name.task_name
268
+
269
+ @property
270
+ def automation_spec(self) -> common_pb2.TriggerAutomationSpec:
271
+ return self.pb2.automation_spec
272
+
273
+ async def get_details(self) -> TriggerDetails:
274
+ """
275
+ Get detailed information about this trigger.
276
+ """
277
+ if not self.details:
278
+ details = await TriggerDetails.get.aio(name=self.pb2.id.name.name)
279
+ self.details = details
280
+ return self.details
281
+
282
+ @property
283
+ def is_active(self) -> bool:
284
+ return self.pb2.active
285
+
286
+ def _rich_automation(self, automation: common_pb2.TriggerAutomationSpec):
287
+ if automation.type == common_pb2.TriggerAutomationSpec.type.TYPE_NONE:
288
+ yield "none", None
289
+ elif automation.type == common_pb2.TriggerAutomationSpec.type.TYPE_SCHEDULE:
290
+ if automation.schedule.cron is not None:
291
+ yield "cron", automation.schedule.cron
292
+ elif automation.schedule.rate is not None:
293
+ r = automation.schedule.rate
294
+ yield (
295
+ "fixed_rate",
296
+ (
297
+ f"Every [{r.value}] {r.unit} starting at "
298
+ f"{r.start_time.ToDatetime() if automation.HasField('start_time') else 'now'}"
299
+ ),
300
+ )
301
+
302
+ def __rich_repr__(self):
303
+ yield "task_name", self.task_name
304
+ yield "name", self.name
305
+ yield from self._rich_automation(self.pb2.automation_spec)
306
+ yield "auto_activate", self.is_active
flyte/remote/_user.py ADDED
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from flyteidl.service import identity_pb2
6
+ from flyteidl.service.identity_pb2 import UserInfoResponse
7
+
8
+ from .._initialize import ensure_client, get_client
9
+ from ..syncify import syncify
10
+ from ._common import ToJSONMixin
11
+
12
+
13
+ @dataclass
14
+ class User(ToJSONMixin):
15
+ pb2: UserInfoResponse
16
+
17
+ @syncify
18
+ @classmethod
19
+ async def get(cls) -> User:
20
+ """
21
+ Fetches information about the currently logged in user.
22
+ Returns: A User object containing details about the user.
23
+ """
24
+ ensure_client()
25
+
26
+ resp = await get_client().identity_service.UserInfo(identity_pb2.UserInfoRequest())
27
+ return cls(resp)
28
+
29
+ def subject(self) -> str:
30
+ return self.pb2.subject
31
+
32
+ def name(self) -> str:
33
+ return self.pb2.name
@@ -0,0 +1,3 @@
1
+ from ._report import Report, current_report, flush, get_tab, log, replace
2
+
3
+ __all__ = ["Report", "current_report", "flush", "get_tab", "log", "replace"]
@@ -0,0 +1,182 @@
1
+ import html
2
+ import pathlib
3
+ import string
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Dict, List, Union
6
+
7
+ from flyte._logging import logger
8
+ from flyte._tools import ipython_check
9
+ from flyte.syncify import syncify
10
+
11
+ if TYPE_CHECKING:
12
+ from IPython.core.display import HTML
13
+
14
+ _MAIN_TAB_NAME = "main"
15
+
16
+
17
+ @dataclass
18
+ class Tab:
19
+ name: str
20
+ content: List[str] = field(default_factory=list, init=False)
21
+
22
+ def log(self, content: str):
23
+ """
24
+ Add content to the tab.
25
+ The content should be a valid HTML string, but not a complete HTML document, as it will be inserted into a div.
26
+
27
+ :param content: The content to add.
28
+ """
29
+ self.content.append(content)
30
+
31
+ def replace(self, content: str):
32
+ """
33
+ Replace the content of the tab.
34
+ The content should be a valid HTML string, but not a complete HTML document, as it will be inserted into a div.
35
+
36
+ :param content: The content to replace.
37
+ """
38
+ self.content = [content]
39
+
40
+ def get_html(self) -> str:
41
+ """
42
+ Get the HTML representation of the tab.
43
+
44
+ :return: The HTML representation of the tab.
45
+ """
46
+ return "\n".join(self.content)
47
+
48
+
49
+ @dataclass
50
+ class Report:
51
+ name: str
52
+ tabs: Dict[str, Tab] = field(default_factory=dict)
53
+ template_path: pathlib.Path = field(default_factory=lambda: pathlib.Path(__file__).parent / "_template.html")
54
+
55
+ def __post_init__(self):
56
+ self.tabs[_MAIN_TAB_NAME] = Tab(_MAIN_TAB_NAME)
57
+
58
+ def get_tab(self, name: str, create_if_missing: bool = True) -> Tab:
59
+ """
60
+ Get a tab by name. If the tab does not exist, create it.
61
+
62
+ :param name: The name of the tab.
63
+ :param create_if_missing: Whether to create the tab if it does not exist.
64
+ :return: The tab.
65
+ """
66
+ if name not in self.tabs:
67
+ if create_if_missing:
68
+ self.tabs[name] = Tab(name)
69
+ else:
70
+ raise ValueError(f"Tab {name} does not exist.")
71
+ return self.tabs[name]
72
+
73
+ def get_final_report(self) -> Union[str, "HTML"]:
74
+ """
75
+ Get the final report as a string.
76
+
77
+ :return: The final report.
78
+ """
79
+ tabs = {n: t.get_html() for n, t in self.tabs.items()}
80
+ nav_htmls = []
81
+ body_htmls = []
82
+
83
+ for key, value in tabs.items():
84
+ nav_htmls.append(f'<li onclick="handleLinkClick(this)">{html.escape(key)}</li>')
85
+ # Can not escape here because this is HTML. Escaping it will present the HTML as text.
86
+ # The renderer must ensure that the HTML is safe.
87
+ body_htmls.append(f"<div>{value}</div>")
88
+
89
+ template = string.Template(self.template_path.open("r").read())
90
+
91
+ raw_html = template.substitute(NAV_HTML="".join(nav_htmls), BODY_HTML="".join(body_htmls))
92
+ if ipython_check():
93
+ try:
94
+ from IPython.core.display import HTML
95
+
96
+ return HTML(raw_html)
97
+ except ImportError:
98
+ ...
99
+ return raw_html
100
+
101
+
102
+ def get_tab(name: str, /, create_if_missing: bool = True) -> Tab:
103
+ """
104
+ Get a tab by name. If the tab does not exist, create it.
105
+
106
+ :param name: The name of the tab.
107
+ :param create_if_missing: Whether to create the tab if it does not exist.
108
+ :return: The tab.
109
+ """
110
+ report = current_report()
111
+ return report.get_tab(name, create_if_missing=create_if_missing)
112
+
113
+
114
+ @syncify
115
+ async def log(content: str, do_flush: bool = False):
116
+ """
117
+ Log content to the main tab. The content should be a valid HTML string, but not a complete HTML document,
118
+ as it will be inserted into a div.
119
+
120
+ :param content: The content to log.
121
+ :param do_flush: flush the report after logging.
122
+ """
123
+ get_tab(_MAIN_TAB_NAME).log(content)
124
+ if do_flush:
125
+ await flush.aio()
126
+
127
+
128
+ @syncify
129
+ async def flush():
130
+ """
131
+ Flush the report.
132
+ """
133
+ import flyte.storage as storage
134
+ from flyte._context import internal_ctx
135
+ from flyte._internal.runtime import io
136
+
137
+ if not internal_ctx().is_task_context():
138
+ return
139
+
140
+ report = internal_ctx().get_report()
141
+ if report is None:
142
+ return
143
+
144
+ report_html = report.get_final_report()
145
+ assert report_html is not None
146
+ assert isinstance(report_html, str)
147
+ report_path = io.report_path(internal_ctx().data.task_context.output_path)
148
+ content_types = {
149
+ "Content-Type": "text/html", # For s3
150
+ "content_type": "text/html", # For gcs
151
+ }
152
+ final_path = await storage.put_stream(report_html.encode("utf-8"), to_path=report_path, attributes=content_types)
153
+ logger.debug(f"Report flushed to {final_path}")
154
+
155
+
156
+ @syncify
157
+ async def replace(content: str, do_flush: bool = False):
158
+ """
159
+ Get the report. Replaces the content of the main tab.
160
+
161
+ :return: The report.
162
+ """
163
+ report = current_report()
164
+ if report is None:
165
+ return
166
+ report.get_tab(_MAIN_TAB_NAME).replace(content)
167
+ if do_flush:
168
+ await flush.aio()
169
+
170
+
171
+ def current_report() -> Report:
172
+ """
173
+ Get the current report. This is a dummy report if not in a task context.
174
+
175
+ :return: The current report.
176
+ """
177
+ from flyte._context import internal_ctx
178
+
179
+ report = internal_ctx().get_report()
180
+ if report is None:
181
+ report = Report("dummy")
182
+ return report
@@ -0,0 +1,124 @@
1
+ <!doctype html>
2
+ <html lang="">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>User Content</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <link href="https://fonts.googleapis.com/css?family=Lato:300,400,700%7COpen+Sans:400,700" rel="stylesheet">
8
+ <style>
9
+ ol, ul {
10
+ list-style: none;
11
+ }
12
+
13
+ table {
14
+ border-collapse: collapse;
15
+ border-spacing: 0;
16
+ }
17
+
18
+ #flyte-frame-nav {
19
+ display: flex;
20
+ width: 100%;
21
+ }
22
+
23
+ #flyte-frame-tabs {
24
+ display: flex;
25
+ width: 100%;
26
+ justify-content: center;
27
+ margin-block: 0;
28
+ padding-inline-start: 0;
29
+ }
30
+
31
+ #flyte-frame-tabs li {
32
+ cursor: pointer;
33
+ padding: 8px;
34
+ margin: 0;
35
+ margin-right: 12px;
36
+ font-size: 14px;
37
+ line-height: 20px;
38
+ font-weight: 700;
39
+ font-style: normal;
40
+ font-family: Open Sans, helvetica, arial, sans-serif;
41
+ color: #666666;
42
+ width: 126px;
43
+ text-align: center;
44
+ }
45
+
46
+ #flyte-frame-tabs li:last-child {
47
+ margin-right: 0;
48
+ }
49
+
50
+ #flyte-frame-tabs li.active {
51
+ border-bottom: 4px solid rgb(163, 26, 255);
52
+ color: #333333;
53
+ }
54
+
55
+ #flyte-frame-container {
56
+ width: auto;
57
+ }
58
+
59
+ #flyte-frame-container > div {
60
+ display: None;
61
+ }
62
+
63
+ #flyte-frame-container > div.active {
64
+ display: block;
65
+ padding: 2rem 2rem;
66
+ }
67
+
68
+ </style>
69
+
70
+ </head>
71
+ <body>
72
+ <nav id="flyte-frame-nav">
73
+ <ul id="flyte-frame-tabs">
74
+ $NAV_HTML
75
+ </ul>
76
+ </nav>
77
+ <div id="flyte-frame-container">
78
+ $BODY_HTML
79
+ </div>
80
+ </body>
81
+ <script>
82
+ const setTabs = index => {
83
+ const container = document.getElementById('flyte-frame-tabs')
84
+ for (let i = 0; i < container.children.length; i++) {
85
+ const tabIndex = container.children[i].getAttribute('link_index')
86
+ if (tabIndex === index) {
87
+ container.children[i].classList.add('active')
88
+ } else {
89
+ container.children[i].className = ''
90
+ }
91
+ }
92
+ }
93
+ const setContent = index => {
94
+ const container = document.getElementById('flyte-frame-container')
95
+ for (let i = 0; i < container.children.length; i++) {
96
+ const tabIndex = container.children[i].getAttribute('link_index')
97
+ if (tabIndex === index) {
98
+ container.children[i].classList.add('active')
99
+ } else {
100
+ container.children[i].className = ''
101
+ }
102
+ }
103
+ }
104
+ const setLinkIndex = index => {
105
+ setTabs(index)
106
+ setContent(index)
107
+ }
108
+ const handleLinkClick = e => {
109
+ const linkIndex = e.getAttribute('link_index');
110
+ setLinkIndex(linkIndex)
111
+ }
112
+
113
+ const tabs = document.getElementById('flyte-frame-tabs');
114
+ const containers = document.getElementById('flyte-frame-container');
115
+ for(var i = 0; i < tabs.children.length; i++) {
116
+ if (i === 0) {
117
+ tabs.children[i].classList.add('active')
118
+ containers.children[i].classList.add('active')
119
+ }
120
+ tabs.children[i].setAttribute("link_index", i+1)
121
+ containers.children[i].setAttribute("link_index", i+1)
122
+ }
123
+ </script>
124
+ </html>
@@ -0,0 +1,36 @@
1
+ __all__ = [
2
+ "ABFS",
3
+ "GCS",
4
+ "S3",
5
+ "Storage",
6
+ "exists",
7
+ "exists_sync",
8
+ "get",
9
+ "get_configured_fsspec_kwargs",
10
+ "get_random_local_directory",
11
+ "get_random_local_path",
12
+ "get_stream",
13
+ "get_underlying_filesystem",
14
+ "is_remote",
15
+ "join",
16
+ "open",
17
+ "put",
18
+ "put_stream",
19
+ ]
20
+
21
+ from ._config import ABFS, GCS, S3, Storage
22
+ from ._storage import (
23
+ exists,
24
+ exists_sync,
25
+ get,
26
+ get_configured_fsspec_kwargs,
27
+ get_random_local_directory,
28
+ get_random_local_path,
29
+ get_stream,
30
+ get_underlying_filesystem,
31
+ is_remote,
32
+ join,
33
+ open,
34
+ put,
35
+ put_stream,
36
+ )