flyte 0.2.0b5__py3-none-any.whl → 0.2.0b8__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.

@@ -0,0 +1,207 @@
1
+ import os
2
+ import pathlib
3
+ import typing
4
+ from dataclasses import dataclass
5
+ from functools import lru_cache
6
+ from os import getenv
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+ from flyte._logging import logger
12
+
13
+ # This is the default config file name for flyte
14
+ FLYTECTL_CONFIG_ENV_VAR = "FLYTECTL_CONFIG"
15
+ UCTL_CONFIG_ENV_VAR = "UCTL_CONFIG"
16
+
17
+
18
+ @dataclass
19
+ class YamlConfigEntry(object):
20
+ """
21
+ Creates a record for the config entry.
22
+ Args:
23
+ switch: dot-delimited string that should match flytectl args. Leaving it as dot-delimited instead of a list
24
+ of strings because it's easier to maintain alignment with flytectl.
25
+ config_value_type: Expected type of the value
26
+ """
27
+
28
+ switch: str
29
+ config_value_type: typing.Type = str
30
+
31
+ def get_env_name(self) -> str:
32
+ var_name = self.switch.upper().replace(".", "_")
33
+ return f"FLYTE_{var_name}"
34
+
35
+ def read_from_env(self, transform: typing.Optional[typing.Callable] = None) -> typing.Optional[typing.Any]:
36
+ """
37
+ Reads the config entry from environment variable, the structure of the env var is current
38
+ ``FLYTE_{SECTION}_{OPTION}`` all upper cased. We will change this in the future.
39
+ :return:
40
+ """
41
+ env = self.get_env_name()
42
+ v = os.environ.get(env, None)
43
+ if v is None:
44
+ return None
45
+ return transform(v) if transform else v
46
+
47
+ def read_from_file(
48
+ self, cfg: "ConfigFile", transform: typing.Optional[typing.Callable] = None
49
+ ) -> typing.Optional[typing.Any]:
50
+ if not cfg:
51
+ return None
52
+ try:
53
+ v = cfg.get(self)
54
+ if isinstance(v, bool) or bool(v is not None and v):
55
+ return transform(v) if transform else v
56
+ except Exception:
57
+ ...
58
+
59
+ return None
60
+
61
+
62
+ @dataclass
63
+ class ConfigEntry(object):
64
+ """
65
+ A top level Config entry holder, that holds multiple different representations of the config.
66
+ Legacy means the INI style config files. YAML support is for the flytectl config file, which is there by default
67
+ when flytectl starts a sandbox
68
+ """
69
+
70
+ yaml_entry: YamlConfigEntry
71
+ transform: typing.Optional[typing.Callable[[str], typing.Any]] = None
72
+
73
+ def read(self, cfg: typing.Optional["ConfigFile"] = None) -> typing.Optional[typing.Any]:
74
+ """
75
+ Reads the config Entry from the various sources in the following order,
76
+ #. First try to read from the relevant environment variable,
77
+ #. If missing, then try to read from the legacy config file, if one was parsed.
78
+ #. If missing, then try to read from the yaml file.
79
+
80
+ The constructor for ConfigFile currently does not allow specification of both the ini and yaml style formats.
81
+
82
+ :param cfg:
83
+ :return:
84
+ """
85
+ from_env = self.yaml_entry.read_from_env(self.transform)
86
+ if from_env is not None:
87
+ return from_env
88
+ if cfg and cfg.yaml_config and self.yaml_entry:
89
+ return self.yaml_entry.read_from_file(cfg, self.transform)
90
+
91
+ return None
92
+
93
+
94
+ class ConfigFile(object):
95
+ def __init__(self, location: str):
96
+ """
97
+ Load the config from this location
98
+ """
99
+ self._location = location
100
+ self._yaml_config = self._read_yaml_config(location)
101
+
102
+ @property
103
+ def path(self) -> pathlib.Path:
104
+ """
105
+ Returns the path to the config file.
106
+ :return: Path to the config file
107
+ """
108
+ return pathlib.Path(self._location)
109
+
110
+ @staticmethod
111
+ def _read_yaml_config(location: str) -> typing.Optional[typing.Dict[str, typing.Any]]:
112
+ with open(location, "r") as fh:
113
+ try:
114
+ yaml_contents = yaml.safe_load(fh)
115
+ return yaml_contents
116
+ except yaml.YAMLError as exc:
117
+ logger.warning(f"Error {exc} reading yaml config file at {location}, ignoring...")
118
+ return None
119
+
120
+ def _get_from_yaml(self, c: YamlConfigEntry) -> typing.Any:
121
+ keys = c.switch.split(".") # flytectl switches are dot delimited
122
+ d = typing.cast(typing.Dict[str, typing.Any], self.yaml_config)
123
+ try:
124
+ for k in keys:
125
+ d = d[k]
126
+ return d
127
+ except KeyError:
128
+ return None
129
+
130
+ def get(self, c: YamlConfigEntry) -> typing.Any:
131
+ return self._get_from_yaml(c)
132
+
133
+ @property
134
+ def yaml_config(self) -> typing.Dict[str, typing.Any] | None:
135
+ return self._yaml_config
136
+
137
+
138
+ def resolve_config_path() -> pathlib.Path | None:
139
+ """
140
+ Config is read from the following locations in order of precedence:
141
+ 1. ./config.yaml if it exists
142
+ 2. `UCTL_CONFIG` environment variable
143
+ 3. `FLYTECTL_CONFIG` environment variable
144
+ 4. ~/.union/config.yaml if it exists
145
+ 5. ~/.flyte/config.yaml if it exists
146
+ """
147
+ current_location_config = Path("config.yaml")
148
+ if current_location_config.exists():
149
+ return current_location_config
150
+ logger.debug("No ./config.yaml found, returning None")
151
+
152
+ uctl_path_from_env = getenv(UCTL_CONFIG_ENV_VAR, None)
153
+ if uctl_path_from_env:
154
+ return pathlib.Path(uctl_path_from_env)
155
+ logger.debug("No UCTL_CONFIG environment variable found, checking FLYTECTL_CONFIG")
156
+
157
+ flytectl_path_from_env = getenv(FLYTECTL_CONFIG_ENV_VAR, None)
158
+ if flytectl_path_from_env:
159
+ return pathlib.Path(flytectl_path_from_env)
160
+ logger.debug("No FLYTECTL_CONFIG environment variable found, checking default locations")
161
+
162
+ home_dir_union_config = Path(Path.home(), ".union", "config.yaml")
163
+ if home_dir_union_config.exists():
164
+ return home_dir_union_config
165
+ logger.debug("No ~/.union/config.yaml found, checking current directory")
166
+
167
+ home_dir_flytectl_config = Path(Path.home(), ".flyte", "config.yaml")
168
+ if home_dir_flytectl_config.exists():
169
+ return home_dir_flytectl_config
170
+ logger.debug("No ~/.flyte/config.yaml found, checking current directory")
171
+
172
+ return None
173
+
174
+
175
+ @lru_cache
176
+ def get_config_file(c: typing.Union[str, ConfigFile, None]) -> ConfigFile | None:
177
+ """
178
+ Checks if the given argument is a file or a configFile and returns a loaded configFile else returns None
179
+ """
180
+ if "PYTEST_VERSION" in os.environ:
181
+ # Use default local config in the pytest environment
182
+ return None
183
+ if isinstance(c, str):
184
+ logger.debug(f"Using specified config file at {c}")
185
+ return ConfigFile(c)
186
+ elif isinstance(c, ConfigFile):
187
+ return c
188
+ config_path = resolve_config_path()
189
+ if config_path:
190
+ return ConfigFile(str(config_path))
191
+ return None
192
+
193
+
194
+ def read_file_if_exists(filename: typing.Optional[str], encoding=None) -> typing.Optional[str]:
195
+ """
196
+ Reads the contents of the file if passed a path. Otherwise, returns None.
197
+
198
+ :param filename: The file path to load
199
+ :param encoding: The encoding to use when reading the file.
200
+ :return: The contents of the file as a string or None.
201
+ """
202
+ if not filename:
203
+ return None
204
+
205
+ file = pathlib.Path(filename)
206
+ logger.debug(f"Reading file contents from [{file}] with current directory [{os.getcwd()}].")
207
+ return file.read_text(encoding=encoding)
@@ -215,7 +215,7 @@ class ContainerTask(TaskTemplate):
215
215
  output_dict[k] = self._convert_output_val_to_correct_type(output_val, output_type)
216
216
  return output_dict
217
217
 
218
- def execute(self, **kwargs) -> Any:
218
+ async def execute(self, **kwargs) -> Any:
219
219
  try:
220
220
  import docker
221
221
  except ImportError:
flyte/remote/_logs.py CHANGED
@@ -49,6 +49,7 @@ class AsyncLogViewer:
49
49
  name: str = "Logs",
50
50
  show_ts: bool = False,
51
51
  filter_system: bool = False,
52
+ panel: bool = False,
52
53
  ):
53
54
  self.console = Console()
54
55
  self.log_source = log_source
@@ -58,12 +59,15 @@ class AsyncLogViewer:
58
59
  self.show_ts = show_ts
59
60
  self.total_lines = 0
60
61
  self.filter_flyte = filter_system
62
+ self.panel = panel
61
63
 
62
- def _render(self) -> Panel:
64
+ def _render(self) -> Panel | Text:
63
65
  log_text = Text()
64
66
  for line in self.lines:
65
67
  log_text.append(line)
66
- return Panel(log_text, title=self.name, border_style="yellow")
68
+ if self.panel:
69
+ return Panel(log_text, title=self.name, border_style="yellow")
70
+ return log_text
67
71
 
68
72
  async def run(self):
69
73
  with Live(self._render(), refresh_per_second=20, console=self.console) as live:
@@ -139,6 +143,7 @@ class Logs:
139
143
  show_ts: bool = False,
140
144
  raw: bool = False,
141
145
  filter_system: bool = False,
146
+ panel: bool = False,
142
147
  ):
143
148
  """
144
149
  Create a log viewer for a given action ID and attempt.
@@ -149,6 +154,7 @@ class Logs:
149
154
  :param show_ts: Whether to show timestamps in the logs.
150
155
  :param raw: if True, return the raw log lines instead of a viewer.
151
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.
152
158
  """
153
159
  if attempt < 1:
154
160
  raise ValueError("Attempt number must be greater than 0.")
@@ -165,5 +171,6 @@ class Logs:
165
171
  show_ts=show_ts,
166
172
  name=f"{action_id.run.name}:{action_id.name} ({attempt})",
167
173
  filter_system=filter_system,
174
+ panel=panel,
168
175
  )
169
176
  await viewer.run()
flyte/remote/_run.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  from dataclasses import dataclass, field
5
5
  from datetime import datetime, timedelta, timezone
6
- from typing import AsyncGenerator, AsyncIterator, Iterator, Literal, Tuple, Union, cast
6
+ from typing import AsyncGenerator, AsyncIterator, Iterator, List, Literal, Tuple, Union, cast
7
7
 
8
8
  import grpc
9
9
  import rich.repr
@@ -46,33 +46,42 @@ def _action_time_phase(action: run_definition_pb2.Action | run_definition_pb2.Ac
46
46
  yield "error", action.error_info if action.HasField("error_info") else "NA"
47
47
 
48
48
 
49
- def _action_rich_repr(action: run_definition_pb2.Action, root: bool = False) -> rich.repr.Result:
49
+ def _action_rich_repr(action: run_definition_pb2.Action) -> rich.repr.Result:
50
50
  """
51
51
  Rich representation of the action.
52
52
  """
53
- yield "run-name", action.id.run.name
53
+ yield "run", action.id.run.name
54
+ if action.metadata.HasField("task"):
55
+ yield "task", action.metadata.task.id.name
56
+ yield "type", "task"
54
57
  yield "name", action.id.name
55
58
  yield from _action_time_phase(action)
56
- yield "task", action.metadata.task.id.name
57
- if not root:
58
- yield "group", action.metadata.group
59
- yield "parent", action.metadata.parent
59
+ yield "group", action.metadata.group
60
+ yield "parent", action.metadata.parent
61
+ yield "attempts", action.status.attempts
60
62
 
61
63
 
62
- def _action_details_rich_repr(action: run_definition_pb2.ActionDetails, root: bool = False) -> rich.repr.Result:
64
+ def _attempt_rich_repr(action: List[run_definition_pb2.ActionAttempt]) -> rich.repr.Result:
65
+ for attempt in action:
66
+ yield "attempt", attempt.attempt
67
+ yield "phase", run_definition_pb2.Phase.Name(attempt.phase)
68
+ yield "logs_available", attempt.logs_available
69
+
70
+
71
+ def _action_details_rich_repr(action: run_definition_pb2.ActionDetails) -> rich.repr.Result:
63
72
  """
64
73
  Rich representation of the action details.
65
74
  """
66
75
  yield "name", action.id.run.name
67
76
  yield from _action_time_phase(action)
68
- # yield "task", action.metadata.task.id.name
69
77
  yield "task", action.resolved_task_spec.task_template.id.name
70
78
  yield "task_type", action.resolved_task_spec.task_template.type
71
79
  yield "task_version", action.resolved_task_spec.task_template.id.version
72
- if not root:
73
- yield "group", action.metadata.group
74
- yield "parent", action.metadata.parent
75
- # TODO attempt info
80
+ yield "attempts", action.attempts
81
+ yield "error_info", action.error_info if action.HasField("error_info") else "NA"
82
+ yield "phase", run_definition_pb2.Phase.Name(action.status.phase)
83
+ yield "group", action.metadata.group
84
+ yield "parent", action.metadata.parent
76
85
 
77
86
 
78
87
  def _action_done_check(phase: run_definition_pb2.Phase) -> bool:
@@ -270,7 +279,7 @@ class Run:
270
279
  """
271
280
  Rich representation of the Run object.
272
281
  """
273
- yield from _action_rich_repr(self.pb2.action, root=True)
282
+ yield from _action_rich_repr(self.pb2.action)
274
283
 
275
284
  def __repr__(self) -> str:
276
285
  """
@@ -375,7 +384,7 @@ class RunDetails:
375
384
  """
376
385
  Rich representation of the Run object.
377
386
  """
378
- yield from _action_details_rich_repr(self.pb2.action, root=True)
387
+ yield from _action_details_rich_repr(self.pb2.action)
379
388
 
380
389
  def __repr__(self) -> str:
381
390
  """
@@ -657,7 +666,7 @@ class Action:
657
666
  """
658
667
  Rich representation of the Action object.
659
668
  """
660
- yield from _action_rich_repr(self.pb2, root=True)
669
+ yield from _action_rich_repr(self.pb2)
661
670
 
662
671
  def __repr__(self) -> str:
663
672
  """
@@ -911,7 +920,7 @@ class ActionDetails:
911
920
  """
912
921
  Rich representation of the Action object.
913
922
  """
914
- yield from _action_details_rich_repr(self.pb2, root=True)
923
+ yield from _action_details_rich_repr(self.pb2)
915
924
 
916
925
  def __repr__(self) -> str:
917
926
  """
flyte/syncify/__init__.py CHANGED
@@ -1,3 +1,54 @@
1
+ """
2
+ # Syncify Module
3
+ This module provides the `syncify` decorator and the `Syncify` class.
4
+ The decorator can be used to convert asynchronous functions or methods into synchronous ones.
5
+ This is useful for integrating async code into synchronous contexts.
6
+
7
+ Every asynchronous function or method wrapped with `syncify` can be called synchronously using the
8
+ parenthesis `()` operator, or asynchronously using the `.aio()` method.
9
+
10
+ Example::
11
+
12
+ ```python
13
+ from flyte.syncify import syncify
14
+
15
+ @syncify
16
+ async def async_function(x: str) -> str:
17
+ return f"Hello, Async World {x}!"
18
+
19
+
20
+ # now you can call it synchronously
21
+ result = async_function("Async World") # Note: no .aio() needed for sync calls
22
+ print(result)
23
+ # Output: Hello, Async World Async World!
24
+
25
+ # or call it asynchronously
26
+ async def main():
27
+ result = await async_function.aio("World") # Note the use of .aio() for async calls
28
+ print(result)
29
+ ```
30
+
31
+ ## Creating a Syncify Instance
32
+ ```python
33
+ from flyte.syncify. import Syncify
34
+
35
+ syncer = Syncify("my_syncer")
36
+
37
+ # Now you can use `syncer` to decorate your async functions or methods
38
+
39
+ ```
40
+
41
+ ## How does it work?
42
+ The Syncify class wraps asynchronous functions, classmethods, instance methods, and static methods to
43
+ provide a synchronous interface. The wrapped methods are always executed in the context of a background loop,
44
+ whether they are called synchronously or asynchronously. This allows for seamless integration of async code, as
45
+ certain async libraries capture the event loop. An example is grpc.aio, which captures the event loop.
46
+ In such a case, the Syncify class ensures that the async function is executed in the context of the background loop.
47
+
48
+ To use it correctly with grpc.aio, you should wrap every grpc.aio channel creation, and client invocation
49
+ with the same `Syncify` instance. This ensures that the async code runs in the correct event loop context.
50
+ """
51
+
1
52
  from flyte.syncify._api import Syncify
2
53
 
3
54
  syncify = Syncify()
flyte/syncify/_api.py CHANGED
@@ -54,14 +54,13 @@ class _BackgroundLoop:
54
54
  functions or methods synchronously.
55
55
  """
56
56
 
57
- def __init__(self):
58
- self.loop = None
59
- self.thread = threading.Thread(name="syncify_bg", target=self._run, daemon=True)
57
+ def __init__(self, name: str):
58
+ self.loop = asyncio.new_event_loop()
59
+ self.thread = threading.Thread(name=name, target=self._run, daemon=True)
60
60
  self.thread.start()
61
61
  atexit.register(self.stop)
62
62
 
63
63
  def _run(self):
64
- self.loop = asyncio.new_event_loop()
65
64
  asyncio.set_event_loop(self.loop)
66
65
  self.loop.run_forever()
67
66
 
@@ -242,8 +241,8 @@ class Syncify:
242
241
 
243
242
  """
244
243
 
245
- def __init__(self):
246
- self._bg_loop = _BackgroundLoop()
244
+ def __init__(self, name: str = "flyte_syncify"):
245
+ self._bg_loop = _BackgroundLoop(name=name)
247
246
 
248
247
  @overload
249
248
  def __call__(self, func: Callable[P, Awaitable[R_co]]) -> Any: ...
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: flyte
3
+ Version: 0.2.0b8
4
+ Summary: Add your description here
5
+ Author-email: Ketan Umare <kumare3@users.noreply.github.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: aiofiles>=24.1.0
9
+ Requires-Dist: click>=8.2.1
10
+ Requires-Dist: flyteidl==1.15.4b0
11
+ Requires-Dist: cloudpickle>=3.1.1
12
+ Requires-Dist: fsspec>=2025.3.0
13
+ Requires-Dist: grpcio>=1.71.0
14
+ Requires-Dist: obstore>=0.6.0
15
+ Requires-Dist: protobuf>=6.30.1
16
+ Requires-Dist: pydantic>=2.10.6
17
+ Requires-Dist: pyyaml>=6.0.2
18
+ Requires-Dist: rich-click>=1.8.9
19
+ Requires-Dist: httpx>=0.28.1
20
+ Requires-Dist: keyring>=25.6.0
21
+ Requires-Dist: msgpack>=1.1.0
22
+ Requires-Dist: toml>=0.10.2
23
+ Requires-Dist: async-lru>=2.0.5
24
+ Requires-Dist: mashumaro
25
+ Requires-Dist: dataclasses_json
26
+
27
+ # Flyte 2 SDK
28
+
29
+ The next-generation SDK for Flyte.
30
+
31
+ ## Quickstart
32
+ 1. Clone this repo and set `unionv2` to working directory.
33
+ 2. Run `uv venv`, and `source .venv/bin/activate` to create a new virtualenv
34
+ 3. Install the latest version of the SDK by running the following:
35
+
36
+ ```
37
+ uv pip install --no-cache --prerelease=allow --upgrade flyte
38
+ ```
39
+ 4. Create the config and point it to the Dogfood GCP cluster by running the following:
40
+
41
+ ```
42
+ flyte create config --endpoint dns:///dogfood-gcp.cloud-staging.union.ai --org dogfood-gcp --project andrew --domain development
43
+ ```
44
+ This will create a `config.yaml` file in the current directory which will be referenced ahead of any other `config.yaml`s found in your system.
45
+
46
+ 5. Now you can run stuff with the CLI:
47
+
48
+ ```
49
+ flyte run --follow examples/basics/devbox_one.py say_hello_nested
50
+ ```
51
+ Note that the `--follow` command is optional. Use this to stream updates to the terminal!
52
+
53
+
54
+
55
+ ## Hello World Example
56
+
57
+ 1. Only async tasks are supported right now. Style recommended, `import flyte` and then `flyte.TaskEnvironment` etc
58
+ 2. You have to create environment for even a single task
59
+ 3. look at examples/... for various examples.
60
+ 4. For a single script recommend using uv run scripts with metadata headers.
61
+
62
+
63
+ ```python
64
+ import flyte
65
+
66
+ env = flyte.TaskEnvironment(name="hello_world")
67
+
68
+
69
+ @env.task
70
+ async def say_hello(data: str) -> str:
71
+ return f"Hello {data}"
72
+
73
+
74
+ @env.task
75
+ async def say_hello_nested(data: str) -> str:
76
+ return await say_hello.override(resources=flyte.Resources(gpu="A100 80G:4")).execute(data)
77
+
78
+
79
+ if __name__ == "__main__":
80
+ import asyncio
81
+
82
+ # to run pure python - the SDK is not invoked at all
83
+ asyncio.run(say_hello_nested("test"))
84
+
85
+ # To run locally, but run through type system etc
86
+ flyte.init()
87
+ flyte.run(say_hello_nested, "World")
88
+
89
+ # To run remote
90
+ flyte.init(endpoint="dns:///localhost:8090", insecure=True)
91
+ flyte.run(say_hello_nested, "World")
92
+ # It is possible to switch local and remote, but keeping init to have and endpoint, but , changing context during run
93
+ flyte.with_runcontext(mode="local").run(...) # this will run locally only
94
+
95
+ # To run remote with a config
96
+ flyte.init_auto_from_config("config.yaml")
97
+ ```
98
+
99
+ # CLI
100
+ All commands can be run from any root directory.
101
+ For examples, it is not needed to have `__init__.py` in the directory. If you run from a directory, the
102
+ code will automatically package and upload all modules that are imported. You can change the behaviour by using --copy-style flag.
103
+
104
+ ```bash
105
+ flyte run examples/basics/devbox_one.py say_hello --data "World"
106
+ ```
107
+
108
+ To Follow the logs for the a0 action, you can use the `--follow` flag:
109
+
110
+ ```bash
111
+ flyte run --follow examples/basics/devbox_one.py say_hello --data "World"
112
+ ```
113
+ Note follow has to be used with `run` command
114
+
115
+ Change copy style
116
+ ```bash
117
+ flyte run --copy-style examples/basics/devbox_one.py say_hello_nested --data "World"
118
+ ```
119
+
120
+ # Building Images
121
+ ```python
122
+
123
+ import flyte
124
+
125
+ env = flyte.TaskEnvironment(
126
+ name="hello_world",
127
+ image=flyte.Image.auto().with_apt_packages(...).with_pip_packages(...),
128
+ )
129
+
130
+ ```
131
+
132
+ ### Deploy
133
+ ```bash
134
+ flyte deploy examples/basics/devbox_one.py say_hello_nested
135
+ ```
136
+
137
+ CLI shortcuts
138
+
139
+ Get runs
140
+ ```bash
141
+ flyte get run
142
+ ```
143
+ Get specific run
144
+ ```bash
145
+ flyte get run "run-name"
146
+ ```
147
+
148
+ Get run actions
149
+ ```bash
150
+ flyte get actions "run-name"
151
+ ```
152
+
153
+ Get specific action
154
+ ```bash
155
+ flyte get action "run-name" "action-name"
156
+ ```
157
+
158
+ Get action logs
159
+ ```bash
160
+ flyte get logs "run-name" ["action-name"]
161
+ ```
162
+ defaults to root action if no action name is provided
163
+
164
+
165
+ you can run any python script directly within the script module using __main__:
166
+
167
+ ```python
168
+ if __name__ == "__main__":
169
+ import flyte
170
+ flyte.init()
171
+ flyte.run(say_hello_nested, "World")
172
+ ```
173
+
174
+ You can also run from cli
175
+
176
+ you can Also run a uv script with metadata headers:
177
+
178
+ ```bash
179
+ uv run scripts / hello_world.py
180
+ ```