waldiez 0.4.9__py3-none-any.whl → 0.4.11__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 waldiez might be problematic. Click here for more details.

Files changed (38) hide show
  1. waldiez/__init__.py +1 -2
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +88 -50
  4. waldiez/exporter.py +64 -9
  5. waldiez/exporting/core/context.py +12 -0
  6. waldiez/exporting/core/extras/flow_extras.py +2 -14
  7. waldiez/exporting/core/types.py +21 -0
  8. waldiez/exporting/flow/exporter.py +4 -0
  9. waldiez/exporting/flow/factory.py +16 -0
  10. waldiez/exporting/flow/orchestrator.py +12 -0
  11. waldiez/exporting/flow/utils/__init__.py +2 -0
  12. waldiez/exporting/flow/utils/common.py +96 -2
  13. waldiez/exporting/flow/utils/logging.py +5 -6
  14. waldiez/io/mqtt.py +7 -3
  15. waldiez/io/structured.py +5 -1
  16. waldiez/models/common/method_utils.py +1 -1
  17. waldiez/models/tool/tool.py +2 -1
  18. waldiez/runner.py +402 -321
  19. waldiez/running/__init__.py +6 -34
  20. waldiez/running/base_runner.py +907 -0
  21. waldiez/running/environment.py +74 -0
  22. waldiez/running/import_runner.py +424 -0
  23. waldiez/running/patch_io_stream.py +208 -0
  24. waldiez/running/post_run.py +26 -24
  25. waldiez/running/pre_run.py +2 -46
  26. waldiez/running/protocol.py +281 -0
  27. waldiez/running/run_results.py +22 -0
  28. waldiez/running/subprocess_runner.py +100 -0
  29. waldiez/utils/__init__.py +0 -2
  30. waldiez/utils/version.py +4 -2
  31. {waldiez-0.4.9.dist-info → waldiez-0.4.11.dist-info}/METADATA +11 -11
  32. {waldiez-0.4.9.dist-info → waldiez-0.4.11.dist-info}/RECORD +37 -32
  33. waldiez/utils/flaml_warnings.py +0 -17
  34. /waldiez/running/{util.py → utils.py} +0 -0
  35. {waldiez-0.4.9.dist-info → waldiez-0.4.11.dist-info}/WHEEL +0 -0
  36. {waldiez-0.4.9.dist-info → waldiez-0.4.11.dist-info}/entry_points.txt +0 -0
  37. {waldiez-0.4.9.dist-info → waldiez-0.4.11.dist-info}/licenses/LICENSE +0 -0
  38. {waldiez-0.4.9.dist-info → waldiez-0.4.11.dist-info}/licenses/NOTICE.md +0 -0
waldiez/__init__.py CHANGED
@@ -5,7 +5,7 @@
5
5
  from .exporter import WaldiezExporter
6
6
  from .models import Waldiez
7
7
  from .runner import WaldiezRunner
8
- from .utils import check_conflicts, check_flaml_warnings
8
+ from .utils import check_conflicts
9
9
 
10
10
  # flake8: noqa: F401
11
11
  # pylint: disable=import-error,line-too-long
@@ -28,7 +28,6 @@ __waldiez_initialized = False
28
28
  if not __waldiez_initialized:
29
29
  __waldiez_initialized = True
30
30
  check_conflicts()
31
- check_flaml_warnings()
32
31
 
33
32
  __all__ = [
34
33
  "Waldiez",
waldiez/_version.py CHANGED
@@ -5,4 +5,4 @@
5
5
  This file is automatically generated by Hatchling.
6
6
  Do not edit this file directly.
7
7
  """
8
- __version__ = VERSION = "0.4.9"
8
+ __version__ = VERSION = "0.4.11"
waldiez/cli.py CHANGED
@@ -8,7 +8,7 @@
8
8
  import json
9
9
  import os
10
10
  from pathlib import Path
11
- from typing import Any, Optional
11
+ from typing import Optional
12
12
 
13
13
  import anyio
14
14
  import typer
@@ -16,7 +16,6 @@ from dotenv import load_dotenv
16
16
  from typing_extensions import Annotated
17
17
 
18
18
  from .exporter import WaldiezExporter
19
- from .io import StructuredIOStream
20
19
  from .logger import get_logger
21
20
  from .models import Waldiez
22
21
  from .runner import WaldiezRunner
@@ -97,6 +96,20 @@ def run(
97
96
  "If set, running the flow will use structured io stream instead of the default 'input/print' "
98
97
  ),
99
98
  ),
99
+ threaded: bool = typer.Option( # noqa: B008
100
+ False,
101
+ help=(
102
+ "If set, the flow will be run in a separate thread. "
103
+ "This is useful for running flows that require user input or print output."
104
+ ),
105
+ ),
106
+ patch_io: bool = typer.Option( # noqa: B008
107
+ False,
108
+ help=(
109
+ "If set, the flow will patch ag2's IOStream to safe print and input methods. "
110
+ "This is useful for running flows that require user input or print output."
111
+ ),
112
+ ),
100
113
  force: bool = typer.Option( # noqa: B008
101
114
  False,
102
115
  help="Override the output file if it already exists.",
@@ -106,18 +119,35 @@ def run(
106
119
  os.environ["AUTOGEN_USE_DOCKER"] = "0"
107
120
  os.environ["NEP50_DISABLE_WARNING"] = "1"
108
121
  output_path = _get_output_path(output, force)
109
- with file.open("r", encoding="utf-8") as _file:
110
- try:
111
- data = json.load(_file)
112
- except json.decoder.JSONDecodeError as error:
113
- typer.echo("Invalid .waldiez file. Not a valid json?")
114
- raise typer.Exit(code=1) from error
115
- _do_run(
116
- data,
117
- structured=structured,
118
- uploads_root=uploads_root,
119
- output_path=output_path,
120
- )
122
+ try:
123
+ runner = WaldiezRunner.load(file)
124
+ except FileNotFoundError as error:
125
+ typer.echo(f"File not found: {file}")
126
+ raise typer.Exit(code=1) from error
127
+ except json.decoder.JSONDecodeError as error:
128
+ typer.echo("Invalid .waldiez file. Not a valid json?")
129
+ raise typer.Exit(code=1) from error
130
+ except ValueError as error:
131
+ typer.echo(f"Invalid .waldiez file: {error}")
132
+ raise typer.Exit(code=1) from error
133
+ if runner.is_async:
134
+ anyio.run(
135
+ runner.a_run,
136
+ output_path,
137
+ uploads_root,
138
+ structured,
139
+ not patch_io, # skip_patch_io
140
+ False, # skip_mmd
141
+ )
142
+ else:
143
+ runner.run(
144
+ output_path=output_path,
145
+ uploads_root=uploads_root,
146
+ structured_io=structured,
147
+ threaded=threaded,
148
+ skip_patch_io=not patch_io,
149
+ skip_mmd=False,
150
+ )
121
151
 
122
152
 
123
153
  @app.command()
@@ -201,42 +231,50 @@ def check(
201
231
  LOG.success("Waldiez flow seems valid.")
202
232
 
203
233
 
204
- def _do_run(
205
- data: dict[str, Any],
206
- structured: bool,
207
- uploads_root: Optional[Path],
208
- output_path: Optional[Path],
209
- ) -> None:
210
- """Run the Waldiez flow and get the results."""
211
- waldiez = Waldiez.from_dict(data)
212
- if structured:
213
- stream = StructuredIOStream(uploads_root=uploads_root)
214
- with StructuredIOStream.set_default(stream):
215
- runner = WaldiezRunner(waldiez)
216
- if waldiez.is_async:
217
- anyio.run(
218
- runner.a_run,
219
- output_path,
220
- uploads_root,
221
- )
222
- else:
223
- runner.run(
224
- output_path=output_path,
225
- uploads_root=uploads_root,
226
- )
227
- else:
228
- runner = WaldiezRunner(waldiez)
229
- if waldiez.is_async:
230
- anyio.run(
231
- runner.a_run,
232
- output_path,
233
- uploads_root,
234
- )
235
- else:
236
- runner.run(
237
- output_path=output_path,
238
- uploads_root=uploads_root,
239
- )
234
+ # def _do_run(
235
+ # data: dict[str, Any],
236
+ # structured: bool,
237
+ # uploads_root: Optional[Path],
238
+ # output_path: Optional[Path],
239
+ # ) -> None:
240
+ # """Run the Waldiez flow and get the results."""
241
+ # waldiez = Waldiez.from_dict(data)
242
+ # if structured:
243
+ # stream = StructuredIOStream(uploads_root=uploads_root)
244
+ # with StructuredIOStream.set_default(stream):
245
+ # runner = WaldiezRunner(waldiez)
246
+ # if waldiez.is_async:
247
+ # anyio.run(
248
+ # runner.a_run,
249
+ # output_path,
250
+ # uploads_root,
251
+ # True, # structured_io
252
+ # False, # skip_mmd
253
+ # )
254
+ # else:
255
+ # runner.run(
256
+ # output_path=output_path,
257
+ # uploads_root=uploads_root,
258
+ # structured_io=True,
259
+ # skip_mmd=False,
260
+ # )
261
+ # else:
262
+ # runner = WaldiezRunner(waldiez)
263
+ # if waldiez.is_async:
264
+ # anyio.run(
265
+ # runner.a_run,
266
+ # output_path,
267
+ # uploads_root,
268
+ # False, # structured_io
269
+ # False, # skip_mmd
270
+ # )
271
+ # else:
272
+ # runner.run(
273
+ # output_path=output_path,
274
+ # uploads_root=uploads_root,
275
+ # structured_io=False,
276
+ # skip_mmd=False,
277
+ # )
240
278
 
241
279
 
242
280
  def _get_output_path(output: Optional[Path], force: bool) -> Optional[Path]:
waldiez/exporter.py CHANGED
@@ -11,7 +11,6 @@ to trigger the chat(s).
11
11
  """
12
12
 
13
13
  from pathlib import Path
14
- from typing import Union
15
14
 
16
15
  import jupytext # type: ignore[import-untyped]
17
16
  from jupytext.config import ( # type: ignore[import-untyped]
@@ -61,7 +60,10 @@ class WaldiezExporter:
61
60
 
62
61
  def export(
63
62
  self,
64
- path: Union[str, Path],
63
+ path: str | Path,
64
+ structured_io: bool = False,
65
+ uploads_root: Path | None = None,
66
+ skip_patch_io: bool = True,
65
67
  force: bool = False,
66
68
  debug: bool = False,
67
69
  ) -> None:
@@ -69,8 +71,15 @@ class WaldiezExporter:
69
71
 
70
72
  Parameters
71
73
  ----------
72
- path : Union[str, Path]
74
+ path : str | Path
73
75
  The path to export to.
76
+ structured_io : bool, (optional)
77
+ Whether to use structured IO instead of the default 'input/print',
78
+ by default False.
79
+ uploads_root : str | Path | None, (optional)
80
+ The uploads root, to get user-uploaded files, by default None.
81
+ skip_patch_io : bool, (optional)
82
+ Whether to skip patching I/O, by default True.
74
83
  force : bool, (optional)
75
84
  Override the output file if it already exists, by default False.
76
85
  debug : bool, (optional)
@@ -99,23 +108,45 @@ class WaldiezExporter:
99
108
  if extension == ".waldiez":
100
109
  self.to_waldiez(path, debug=debug)
101
110
  elif extension == ".py":
102
- self.to_py(path, debug=debug)
111
+ self.to_py(
112
+ path,
113
+ structured_io=structured_io,
114
+ uploads_root=uploads_root,
115
+ skip_patch_io=skip_patch_io,
116
+ debug=debug,
117
+ )
103
118
  elif extension == ".ipynb":
104
- self.to_ipynb(path, debug=debug)
119
+ self.to_ipynb(
120
+ path,
121
+ structured_io=structured_io,
122
+ uploads_root=uploads_root,
123
+ skip_patch_io=skip_patch_io,
124
+ debug=debug,
125
+ )
105
126
  else:
106
127
  raise ValueError(f"Invalid extension: {extension}")
107
128
 
108
129
  def to_ipynb(
109
130
  self,
110
- path: Path,
131
+ path: str | Path,
132
+ structured_io: bool = False,
133
+ uploads_root: Path | None = None,
134
+ skip_patch_io: bool = True,
111
135
  debug: bool = False,
112
136
  ) -> None:
113
137
  """Export flow to jupyter notebook.
114
138
 
115
139
  Parameters
116
140
  ----------
117
- path : Path
141
+ path : str | Path
118
142
  The path to export to.
143
+ structured_io : bool, optional
144
+ Whether to use structured IO instead of the default 'input/print',
145
+ by default False.
146
+ uploads_root : Path | None, optional
147
+ The uploads root, to get user-uploaded files, by default None.
148
+ skip_patch_io : bool, optional
149
+ Whether to skip patching I/O, by default True.
119
150
  debug : bool, optional
120
151
  Whether to enable debug mode, by default False.
121
152
 
@@ -126,10 +157,15 @@ class WaldiezExporter:
126
157
  """
127
158
  # we first create a .py file with the content
128
159
  # and then convert it to a notebook using jupytext
160
+ if not isinstance(path, Path):
161
+ path = Path(path)
129
162
  exporter = create_flow_exporter(
130
163
  waldiez=self.waldiez,
131
164
  output_dir=path.parent,
165
+ uploads_root=uploads_root,
166
+ structured_io=structured_io,
132
167
  for_notebook=True,
168
+ skip_patch_io=skip_patch_io,
133
169
  debug=debug,
134
170
  )
135
171
  self.flow_extras = exporter.extras
@@ -161,13 +197,27 @@ class WaldiezExporter:
161
197
  Path(ipynb_path).rename(ipynb_path.replace(".tmp.ipynb", ".ipynb"))
162
198
  py_path.unlink(missing_ok=True)
163
199
 
164
- def to_py(self, path: Path, debug: bool = False) -> None:
200
+ def to_py(
201
+ self,
202
+ path: str | Path,
203
+ structured_io: bool = False,
204
+ uploads_root: Path | None = None,
205
+ skip_patch_io: bool = True,
206
+ debug: bool = False,
207
+ ) -> None:
165
208
  """Export waldiez flow to a python script.
166
209
 
167
210
  Parameters
168
211
  ----------
169
- path : Path
212
+ path : str | Path
170
213
  The path to export to.
214
+ structured_io : bool, optional
215
+ Whether to use structured IO instead of the default 'input/print',
216
+ by default False.
217
+ uploads_root : Path | None, optional
218
+ The uploads root, to get user-uploaded files, by default None.
219
+ skip_patch_io : bool, optional
220
+ Whether to skip patching I/O, by default True.
171
221
  debug : bool, optional
172
222
  Whether to enable debug mode, by default False.
173
223
 
@@ -176,10 +226,15 @@ class WaldiezExporter:
176
226
  RuntimeError
177
227
  If the python script could not be generated.
178
228
  """
229
+ if not isinstance(path, Path):
230
+ path = Path(path)
179
231
  exporter = create_flow_exporter(
180
232
  waldiez=self.waldiez,
181
233
  output_dir=path.parent,
182
234
  for_notebook=False,
235
+ uploads_root=uploads_root,
236
+ structured_io=structured_io,
237
+ skip_patch_io=skip_patch_io,
183
238
  debug=debug,
184
239
  )
185
240
  self.flow_extras = exporter.extras
@@ -54,6 +54,8 @@ class ExporterContext:
54
54
  is_async: bool = False,
55
55
  output_directory: Optional[str] = None,
56
56
  cache_seed: Optional[int] = None,
57
+ structured_io: bool = False,
58
+ skip_patch_io: bool = True,
57
59
  ) -> ExportConfig:
58
60
  """Get export config or return default.
59
61
 
@@ -75,6 +77,10 @@ class ExporterContext:
75
77
  The directory where the output will be saved, by default None
76
78
  cache_seed : Optional[int], optional
77
79
  The seed for caching, by default None
80
+ structured_io : bool, optional
81
+ Whether to use structured I/O, by default False
82
+ skip_patch_io : bool, optional
83
+ Whether to skip patching I/O, by default True
78
84
 
79
85
  Returns
80
86
  -------
@@ -85,6 +91,12 @@ class ExporterContext:
85
91
  "requirements": requirements or [],
86
92
  "tags": tags or [],
87
93
  "is_async": self.config.is_async if self.config else is_async,
94
+ "structured_io": (
95
+ self.config.structured_io if self.config else structured_io
96
+ ),
97
+ "skip_patch_io": (
98
+ self.config.skip_patch_io if self.config else skip_patch_io
99
+ ),
88
100
  }
89
101
  if output_extension is not None:
90
102
  kwargs["output_extension"] = output_extension
@@ -7,19 +7,7 @@
7
7
  from dataclasses import dataclass, field
8
8
  from typing import Optional
9
9
 
10
- # pylint: disable=import-error,line-too-long
11
- # pyright: reportMissingImports=false
12
- try:
13
- from waldiez._version import __version__ as waldiez_version # type: ignore[unused-ignore, import-not-found, import-untyped] # noqa
14
- except ImportError: # pragma: no cover
15
- import warnings
16
-
17
- # .gitingored (generated by hatch)
18
- warnings.warn(
19
- "Importing __version__ failed. Using 'dev' as version.",
20
- stacklevel=2,
21
- )
22
- waldiez_version = "dev"
10
+ from waldiez.utils import get_waldiez_version
23
11
 
24
12
  from ..result import ExportResult
25
13
  from ..types import ExportConfig
@@ -34,7 +22,7 @@ class FlowExtras(BaseExtras):
34
22
  flow_name: str = ""
35
23
  description: str = ""
36
24
  config: ExportConfig = field(default_factory=ExportConfig)
37
- version: str = waldiez_version # pyright: ignore
25
+ version: str = field(default_factory=get_waldiez_version)
38
26
 
39
27
  # Sub-exporter results
40
28
  tools_result: Optional[ExportResult] = None
@@ -239,10 +239,28 @@ class ExportConfig:
239
239
 
240
240
  Attributes
241
241
  ----------
242
+ name : str
243
+ The name of the export.
244
+ description : str
245
+ A brief description of the export.
246
+ requirements : list[str]
247
+ A list of requirements for the export.
248
+ tags : list[str]
249
+ A list of tags associated with the export.
250
+ output_directory : Optional[str | Path]
251
+ The directory where the exported content will be saved.
252
+ uploads_root : Optional[str | Path]
253
+ The root directory for uploads, if applicable.
254
+ cache_seed : Optional[int]
255
+ The seed for caching, if applicable.
256
+ structured_io : bool
257
+ Whether the export should use structured I/O.
242
258
  output_extension : str
243
259
  The file extension for the exported content.
244
260
  is_async : bool
245
261
  Whether the exported content should be asynchronous.
262
+ skip_patch_io : bool
263
+ Whether to skip patching I/O operations.
246
264
  """
247
265
 
248
266
  name: str = "Waldiez Flow"
@@ -254,7 +272,10 @@ class ExportConfig:
254
272
  output_extension: str = "py"
255
273
  is_async: bool = False
256
274
  output_directory: Optional[str | Path] = None
275
+ uploads_root: Optional[Path] = None
257
276
  cache_seed: Optional[int] = None
277
+ structured_io: bool = False
278
+ skip_patch_io: bool = True
258
279
 
259
280
  @property
260
281
  def for_notebook(self) -> bool:
@@ -21,6 +21,8 @@ class FlowExporter(Exporter[FlowExtras]):
21
21
  waldiez: Waldiez,
22
22
  output_dir: Path | None,
23
23
  for_notebook: bool,
24
+ structured_io: bool = False,
25
+ skip_patch_io: bool = True,
24
26
  context: Optional[ExporterContext] = None,
25
27
  **kwargs: Any,
26
28
  ) -> None:
@@ -52,6 +54,8 @@ class FlowExporter(Exporter[FlowExtras]):
52
54
  is_async=waldiez.is_async,
53
55
  output_directory=str(self.output_dir) if self.output_dir else None,
54
56
  cache_seed=waldiez.cache_seed,
57
+ structured_io=structured_io,
58
+ skip_patch_io=skip_patch_io,
55
59
  )
56
60
  self._extras = self._generate_extras()
57
61
 
@@ -20,6 +20,7 @@ from .exporter import FlowExporter
20
20
  def create_flow_exporter(
21
21
  waldiez: Waldiez,
22
22
  output_dir: Path | None,
23
+ uploads_root: Path | None,
23
24
  for_notebook: bool,
24
25
  context: Optional[ExporterContext] = None,
25
26
  **kwargs: Any,
@@ -32,6 +33,8 @@ def create_flow_exporter(
32
33
  The Waldiez instance containing the flow data.
33
34
  output_dir : Path
34
35
  The directory where the exported flow will be saved.
36
+ uploads_root : Path
37
+ The root directory for uploads, if applicable.
35
38
  for_notebook : bool
36
39
  Whether the export is intended for a notebook environment.
37
40
  context : Optional[ExporterContext], optional
@@ -44,6 +47,8 @@ def create_flow_exporter(
44
47
  ChatsExporter
45
48
  The created chats exporter.
46
49
  """
50
+ structured_io = kwargs.pop("structured_io", False)
51
+ skip_patch_io = kwargs.pop("skip_patch_io", True)
47
52
  if context is None:
48
53
  config = ExportConfig(
49
54
  name=waldiez.name,
@@ -53,7 +58,10 @@ def create_flow_exporter(
53
58
  output_extension="ipynb" if for_notebook else "py",
54
59
  is_async=waldiez.is_async,
55
60
  output_directory=output_dir,
61
+ uploads_root=uploads_root,
56
62
  cache_seed=waldiez.cache_seed,
63
+ structured_io=structured_io,
64
+ skip_patch_io=skip_patch_io,
57
65
  )
58
66
  context = ExporterContext(
59
67
  config=config,
@@ -71,7 +79,10 @@ def create_flow_exporter(
71
79
  output_extension="ipynb" if for_notebook else "py",
72
80
  is_async=waldiez.is_async,
73
81
  output_directory=output_dir,
82
+ uploads_root=uploads_root,
74
83
  cache_seed=waldiez.cache_seed,
84
+ structured_io=structured_io,
85
+ skip_patch_io=skip_patch_io,
75
86
  )
76
87
  else:
77
88
  context.config.update(
@@ -82,13 +93,18 @@ def create_flow_exporter(
82
93
  output_extension="ipynb" if for_notebook else "py",
83
94
  is_async=waldiez.is_async,
84
95
  output_directory=output_dir,
96
+ uploads_root=uploads_root,
85
97
  cache_seed=waldiez.cache_seed,
98
+ structured_io=structured_io,
99
+ skip_patch_io=skip_patch_io,
86
100
  )
87
101
 
88
102
  return FlowExporter(
89
103
  waldiez=waldiez,
90
104
  output_dir=output_dir,
105
+ uploads_root=uploads_root,
91
106
  for_notebook=for_notebook,
107
+ structured_io=structured_io,
92
108
  context=context,
93
109
  **kwargs,
94
110
  )
@@ -23,6 +23,7 @@ from .utils import (
23
23
  generate_header,
24
24
  get_after_run_content,
25
25
  get_np_no_nep50_handle,
26
+ get_set_io_stream,
26
27
  get_sqlite_out,
27
28
  get_start_logging,
28
29
  get_stop_logging,
@@ -182,6 +183,17 @@ class ExportOrchestrator:
182
183
  order=ContentOrder.CLEANUP.value + 1, # after imports
183
184
  skip_strip=True, # keep newlines
184
185
  )
186
+ if not self.config.skip_patch_io:
187
+ merged_result.add_content(
188
+ get_set_io_stream(
189
+ use_structured_io=self.config.structured_io,
190
+ is_async=self.waldiez.is_async,
191
+ uploads_root=self.config.uploads_root,
192
+ ),
193
+ position=ExportPosition.IMPORTS, # after imports, before models
194
+ order=ContentOrder.CLEANUP.value + 2, # after start logging
195
+ )
196
+ # merged_result.add_content
185
197
  merged_result.add_content(
186
198
  get_sqlite_out(is_async=self.waldiez.is_async),
187
199
  position=ExportPosition.AGENTS,
@@ -6,6 +6,7 @@ from .common import (
6
6
  generate_header,
7
7
  get_after_run_content,
8
8
  get_np_no_nep50_handle,
9
+ get_set_io_stream,
9
10
  )
10
11
  from .importing import get_sorted_imports, get_the_imports_string
11
12
  from .logging import get_sqlite_out, get_start_logging, get_stop_logging
@@ -19,4 +20,5 @@ __all__ = [
19
20
  "get_start_logging",
20
21
  "get_stop_logging",
21
22
  "get_sqlite_out",
23
+ "get_set_io_stream",
22
24
  ]