torchx-nightly 2025.9.3__py3-none-any.whl → 2025.9.5__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 torchx-nightly might be problematic. Click here for more details.
- torchx/cli/cmd_run.py +122 -20
- torchx/runner/api.py +7 -3
- torchx/runner/config.py +1 -1
- torchx/specs/__init__.py +3 -0
- torchx/specs/builders.py +5 -1
- {torchx_nightly-2025.9.3.dist-info → torchx_nightly-2025.9.5.dist-info}/METADATA +1 -1
- {torchx_nightly-2025.9.3.dist-info → torchx_nightly-2025.9.5.dist-info}/RECORD +11 -11
- {torchx_nightly-2025.9.3.dist-info → torchx_nightly-2025.9.5.dist-info}/LICENSE +0 -0
- {torchx_nightly-2025.9.3.dist-info → torchx_nightly-2025.9.5.dist-info}/WHEEL +0 -0
- {torchx_nightly-2025.9.3.dist-info → torchx_nightly-2025.9.5.dist-info}/entry_points.txt +0 -0
- {torchx_nightly-2025.9.3.dist-info → torchx_nightly-2025.9.5.dist-info}/top_level.txt +0 -0
torchx/cli/cmd_run.py
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
# pyre-strict
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
|
+
import json
|
|
10
11
|
import logging
|
|
11
12
|
import os
|
|
12
13
|
import sys
|
|
@@ -41,6 +42,12 @@ MISSING_COMPONENT_ERROR_MSG = (
|
|
|
41
42
|
"missing component name, either provide it from the CLI or in .torchxconfig"
|
|
42
43
|
)
|
|
43
44
|
|
|
45
|
+
LOCAL_SCHEDULER_WARNING_MSG = (
|
|
46
|
+
"`local` scheduler is deprecated and will be"
|
|
47
|
+
" removed in the near future,"
|
|
48
|
+
" please use other variants of the local scheduler"
|
|
49
|
+
" (e.g. `local_cwd`)"
|
|
50
|
+
)
|
|
44
51
|
|
|
45
52
|
logger: logging.Logger = logging.getLogger(__name__)
|
|
46
53
|
|
|
@@ -54,7 +61,7 @@ class TorchXRunArgs:
|
|
|
54
61
|
dryrun: bool = False
|
|
55
62
|
wait: bool = False
|
|
56
63
|
log: bool = False
|
|
57
|
-
workspace: str =
|
|
64
|
+
workspace: str = ""
|
|
58
65
|
parent_run_id: Optional[str] = None
|
|
59
66
|
tee_logs: bool = False
|
|
60
67
|
component_args: Dict[str, Any] = field(default_factory=dict)
|
|
@@ -83,7 +90,10 @@ def torchx_run_args_from_json(json_data: Dict[str, Any]) -> TorchXRunArgs:
|
|
|
83
90
|
"Please check your JSON and try launching again.",
|
|
84
91
|
)
|
|
85
92
|
|
|
86
|
-
|
|
93
|
+
torchx_args = TorchXRunArgs(**filtered_json_data)
|
|
94
|
+
if torchx_args.workspace == "":
|
|
95
|
+
torchx_args.workspace = f"file://{Path.cwd()}"
|
|
96
|
+
return torchx_args
|
|
87
97
|
|
|
88
98
|
|
|
89
99
|
def torchx_run_args_from_argparse(
|
|
@@ -196,6 +206,7 @@ class CmdBuiltins(SubCommand):
|
|
|
196
206
|
class CmdRun(SubCommand):
|
|
197
207
|
def __init__(self) -> None:
|
|
198
208
|
self._subparser: Optional[argparse.ArgumentParser] = None
|
|
209
|
+
self._stdin_data_json: Optional[Dict[str, Any]] = None
|
|
199
210
|
|
|
200
211
|
def add_arguments(self, subparser: argparse.ArgumentParser) -> None:
|
|
201
212
|
scheduler_names = get_scheduler_factories().keys()
|
|
@@ -256,35 +267,35 @@ class CmdRun(SubCommand):
|
|
|
256
267
|
default=False,
|
|
257
268
|
help="Add additional prefix to log lines to indicate which replica is printing the log",
|
|
258
269
|
)
|
|
270
|
+
subparser.add_argument(
|
|
271
|
+
"--stdin",
|
|
272
|
+
action="store_true",
|
|
273
|
+
default=False,
|
|
274
|
+
help="Read JSON input from stdin to parse into torchx run args and run the component.",
|
|
275
|
+
)
|
|
259
276
|
subparser.add_argument(
|
|
260
277
|
"component_name_and_args",
|
|
261
278
|
nargs=argparse.REMAINDER,
|
|
262
279
|
)
|
|
263
280
|
|
|
264
|
-
def
|
|
281
|
+
def _run_inner(self, runner: Runner, args: TorchXRunArgs) -> None:
|
|
265
282
|
if args.scheduler == "local":
|
|
266
|
-
logger.warning(
|
|
267
|
-
"`local` scheduler is deprecated and will be"
|
|
268
|
-
" removed in the near future,"
|
|
269
|
-
" please use other variants of the local scheduler"
|
|
270
|
-
" (e.g. `local_cwd`)"
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
cfg = dict(runner.cfg_from_str(args.scheduler, args.scheduler_args))
|
|
274
|
-
config.apply(scheduler=args.scheduler, cfg=cfg)
|
|
283
|
+
logger.warning(LOCAL_SCHEDULER_WARNING_MSG)
|
|
275
284
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
285
|
+
config.apply(scheduler=args.scheduler, cfg=args.scheduler_cfg)
|
|
286
|
+
component_args = (
|
|
287
|
+
args.component_args_str
|
|
288
|
+
if args.component_args_str != []
|
|
289
|
+
else args.component_args
|
|
279
290
|
)
|
|
280
291
|
try:
|
|
281
292
|
if args.dryrun:
|
|
282
293
|
dryrun_info = runner.dryrun_component(
|
|
283
|
-
|
|
294
|
+
args.component_name,
|
|
284
295
|
component_args,
|
|
285
296
|
args.scheduler,
|
|
286
297
|
workspace=args.workspace,
|
|
287
|
-
cfg=
|
|
298
|
+
cfg=args.scheduler_cfg,
|
|
288
299
|
parent_run_id=args.parent_run_id,
|
|
289
300
|
)
|
|
290
301
|
print(
|
|
@@ -295,11 +306,11 @@ class CmdRun(SubCommand):
|
|
|
295
306
|
print("\n=== SCHEDULER REQUEST ===\n" f"{dryrun_info}")
|
|
296
307
|
else:
|
|
297
308
|
app_handle = runner.run_component(
|
|
298
|
-
|
|
309
|
+
args.component_name,
|
|
299
310
|
component_args,
|
|
300
311
|
args.scheduler,
|
|
301
312
|
workspace=args.workspace,
|
|
302
|
-
cfg=
|
|
313
|
+
cfg=args.scheduler_cfg,
|
|
303
314
|
parent_run_id=args.parent_run_id,
|
|
304
315
|
)
|
|
305
316
|
# DO NOT delete this line. It is used by slurm tests to retrieve the app id
|
|
@@ -320,7 +331,9 @@ class CmdRun(SubCommand):
|
|
|
320
331
|
)
|
|
321
332
|
|
|
322
333
|
except (ComponentValidationException, ComponentNotFoundException) as e:
|
|
323
|
-
error_msg =
|
|
334
|
+
error_msg = (
|
|
335
|
+
f"\nFailed to run component `{args.component_name}` got errors: \n {e}"
|
|
336
|
+
)
|
|
324
337
|
logger.error(error_msg)
|
|
325
338
|
sys.exit(1)
|
|
326
339
|
except specs.InvalidRunConfigException as e:
|
|
@@ -335,6 +348,95 @@ class CmdRun(SubCommand):
|
|
|
335
348
|
print(error_msg % (e, args.scheduler, args.scheduler), file=sys.stderr)
|
|
336
349
|
sys.exit(1)
|
|
337
350
|
|
|
351
|
+
def _run_from_cli_args(self, runner: Runner, args: argparse.Namespace) -> None:
|
|
352
|
+
scheduler_opts = runner.scheduler_run_opts(args.scheduler)
|
|
353
|
+
cfg = scheduler_opts.cfg_from_str(args.scheduler_args)
|
|
354
|
+
|
|
355
|
+
component, component_args = _parse_component_name_and_args(
|
|
356
|
+
args.component_name_and_args,
|
|
357
|
+
none_throws(self._subparser),
|
|
358
|
+
)
|
|
359
|
+
torchx_run_args = torchx_run_args_from_argparse(
|
|
360
|
+
args, component, component_args, cfg
|
|
361
|
+
)
|
|
362
|
+
self._run_inner(runner, torchx_run_args)
|
|
363
|
+
|
|
364
|
+
def _run_from_stdin_args(self, runner: Runner, stdin_data: Dict[str, Any]) -> None:
|
|
365
|
+
torchx_run_args = torchx_run_args_from_json(stdin_data)
|
|
366
|
+
scheduler_opts = runner.scheduler_run_opts(torchx_run_args.scheduler)
|
|
367
|
+
cfg = scheduler_opts.cfg_from_json_repr(
|
|
368
|
+
json.dumps(torchx_run_args.scheduler_args)
|
|
369
|
+
)
|
|
370
|
+
torchx_run_args.scheduler_cfg = cfg
|
|
371
|
+
self._run_inner(runner, torchx_run_args)
|
|
372
|
+
|
|
373
|
+
def _get_torchx_stdin_args(
|
|
374
|
+
self, args: argparse.Namespace
|
|
375
|
+
) -> Optional[Dict[str, Any]]:
|
|
376
|
+
if not args.stdin:
|
|
377
|
+
return None
|
|
378
|
+
if self._stdin_data_json is None:
|
|
379
|
+
self._stdin_data_json = self.torchx_json_from_stdin()
|
|
380
|
+
return self._stdin_data_json
|
|
381
|
+
|
|
382
|
+
def torchx_json_from_stdin(self) -> Dict[str, Any]:
|
|
383
|
+
try:
|
|
384
|
+
stdin_data_json = json.load(sys.stdin)
|
|
385
|
+
if not isinstance(stdin_data_json, dict):
|
|
386
|
+
logger.error(
|
|
387
|
+
"Invalid JSON input for `torchx run` command. Expected a dictionary."
|
|
388
|
+
)
|
|
389
|
+
sys.exit(1)
|
|
390
|
+
return stdin_data_json
|
|
391
|
+
except (json.JSONDecodeError, EOFError):
|
|
392
|
+
logger.error(
|
|
393
|
+
"Unable to parse JSON input for `torchx run` command, please make sure it's a valid JSON input."
|
|
394
|
+
)
|
|
395
|
+
sys.exit(1)
|
|
396
|
+
|
|
397
|
+
def verify_no_extra_args(self, args: argparse.Namespace) -> None:
|
|
398
|
+
"""
|
|
399
|
+
Verifies that only --stdin was provided when using stdin mode.
|
|
400
|
+
"""
|
|
401
|
+
if not args.stdin:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
subparser = none_throws(self._subparser)
|
|
405
|
+
conflicting_args = []
|
|
406
|
+
|
|
407
|
+
# Check each argument against its default value
|
|
408
|
+
for action in subparser._actions:
|
|
409
|
+
if action.dest == "stdin": # Skip stdin itself
|
|
410
|
+
continue
|
|
411
|
+
if action.dest == "help": # Skip help
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
current_value = getattr(args, action.dest, None)
|
|
415
|
+
default_value = action.default
|
|
416
|
+
|
|
417
|
+
# For arguments that differ from default
|
|
418
|
+
if current_value != default_value:
|
|
419
|
+
# Handle special cases where non-default doesn't mean explicitly set
|
|
420
|
+
if action.dest == "component_name_and_args" and current_value == []:
|
|
421
|
+
continue # Empty list is still default
|
|
422
|
+
print(f"*********\n {default_value} = {current_value}")
|
|
423
|
+
conflicting_args.append(f"--{action.dest.replace('_', '-')}")
|
|
424
|
+
|
|
425
|
+
if conflicting_args:
|
|
426
|
+
subparser.error(
|
|
427
|
+
f"Cannot specify {', '.join(conflicting_args)} when using --stdin. "
|
|
428
|
+
"All configuration should be provided in JSON input."
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
def _run(self, runner: Runner, args: argparse.Namespace) -> None:
|
|
432
|
+
self.verify_no_extra_args(args)
|
|
433
|
+
if args.stdin:
|
|
434
|
+
stdin_data_json = self._get_torchx_stdin_args(args)
|
|
435
|
+
if stdin_data_json is not None:
|
|
436
|
+
self._run_from_stdin_args(runner, stdin_data_json)
|
|
437
|
+
else:
|
|
438
|
+
self._run_from_cli_args(runner, args)
|
|
439
|
+
|
|
338
440
|
def run(self, args: argparse.Namespace) -> None:
|
|
339
441
|
os.environ["TORCHX_CONTEXT_NAME"] = os.getenv("TORCHX_CONTEXT_NAME", "cli_run")
|
|
340
442
|
component_defaults = load_sections(prefix="component")
|
torchx/runner/api.py
CHANGED
|
@@ -25,6 +25,7 @@ from typing import (
|
|
|
25
25
|
Type,
|
|
26
26
|
TYPE_CHECKING,
|
|
27
27
|
TypeVar,
|
|
28
|
+
Union,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
from torchx.runner.events import log_event
|
|
@@ -167,7 +168,7 @@ class Runner:
|
|
|
167
168
|
def run_component(
|
|
168
169
|
self,
|
|
169
170
|
component: str,
|
|
170
|
-
component_args:
|
|
171
|
+
component_args: Union[list[str], dict[str, Any]],
|
|
171
172
|
scheduler: str,
|
|
172
173
|
cfg: Optional[Mapping[str, CfgVal]] = None,
|
|
173
174
|
workspace: Optional[str] = None,
|
|
@@ -226,7 +227,7 @@ class Runner:
|
|
|
226
227
|
def dryrun_component(
|
|
227
228
|
self,
|
|
228
229
|
component: str,
|
|
229
|
-
component_args:
|
|
230
|
+
component_args: Union[list[str], dict[str, Any]],
|
|
230
231
|
scheduler: str,
|
|
231
232
|
cfg: Optional[Mapping[str, CfgVal]] = None,
|
|
232
233
|
workspace: Optional[str] = None,
|
|
@@ -237,10 +238,13 @@ class Runner:
|
|
|
237
238
|
component, but just returns what "would" have run.
|
|
238
239
|
"""
|
|
239
240
|
component_def = get_component(component)
|
|
241
|
+
args_from_cli = component_args if isinstance(component_args, list) else []
|
|
242
|
+
args_from_json = component_args if isinstance(component_args, dict) else {}
|
|
240
243
|
app = materialize_appdef(
|
|
241
244
|
component_def.fn,
|
|
242
|
-
|
|
245
|
+
args_from_cli,
|
|
243
246
|
self._component_defaults.get(component, None),
|
|
247
|
+
args_from_json,
|
|
244
248
|
)
|
|
245
249
|
return self.dryrun(
|
|
246
250
|
app,
|
torchx/runner/config.py
CHANGED
|
@@ -73,7 +73,7 @@ CLI Usage
|
|
|
73
73
|
|
|
74
74
|
#. In addition, it is possible to specify a different config other than .torchxconfig to
|
|
75
75
|
load at runtime. Requirements are that the config path is specified by enviornment
|
|
76
|
-
variable
|
|
76
|
+
variable TORCHXCONFIG. It also disables hierarchy loading configs from multiple
|
|
77
77
|
directories as the cases otherwise.
|
|
78
78
|
|
|
79
79
|
#. User level .torchxconfig
|
torchx/specs/__init__.py
CHANGED
torchx/specs/builders.py
CHANGED
|
@@ -213,7 +213,11 @@ def component_args_from_str(
|
|
|
213
213
|
arg_value = getattr(parsed_args, param_name)
|
|
214
214
|
parameter_type = parameter.annotation
|
|
215
215
|
parameter_type = decode_optional(parameter_type)
|
|
216
|
-
|
|
216
|
+
if (
|
|
217
|
+
parameter_type != arg_value.__class__
|
|
218
|
+
and parameter.kind != inspect.Parameter.VAR_POSITIONAL
|
|
219
|
+
):
|
|
220
|
+
arg_value = decode(arg_value, parameter_type)
|
|
217
221
|
if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
218
222
|
var_args = arg_value
|
|
219
223
|
elif parameter.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
@@ -16,7 +16,7 @@ torchx/cli/cmd_configure.py,sha256=1kTv0qbsbV44So74plAySwWu56pQrqjhfW_kbfdC3Rw,1
|
|
|
16
16
|
torchx/cli/cmd_describe.py,sha256=E5disbHoKTsqYKp2s3DaFW9GDLCCOgdOc3pQoHKoyCs,1283
|
|
17
17
|
torchx/cli/cmd_list.py,sha256=4Y1ZOq-kqJbztoBt56hAW_InJEaJuDAjpKWgMhBw4II,1507
|
|
18
18
|
torchx/cli/cmd_log.py,sha256=v-EZYUDOcG95rEgTnrsmPJMUyxM9Mk8YFAJtUxtgViE,5475
|
|
19
|
-
torchx/cli/cmd_run.py,sha256=
|
|
19
|
+
torchx/cli/cmd_run.py,sha256=Ugbn6PUmfVbSlzcrwXSQmdLOEzVnjS5bBUJ5v2Djw0s,18468
|
|
20
20
|
torchx/cli/cmd_runopts.py,sha256=NWZiP8XpQjfTDJgays2c6MgL_8wxFoeDge6NstaZdKk,1302
|
|
21
21
|
torchx/cli/cmd_status.py,sha256=22IAEmKs0qkG6kJi83u9dRX2Q-ntT7yehVx7FxtY-vQ,2114
|
|
22
22
|
torchx/cli/cmd_tracker.py,sha256=RfLxE4Cq1wfk7k051RtZ8RPJp0pEKSCa3KmTeRs3LF8,5218
|
|
@@ -56,8 +56,8 @@ torchx/pipelines/kfp/__init__.py,sha256=8iJ8lql_fxwuk9VCYSxXnX6tPL228fB5mDZpOs-k
|
|
|
56
56
|
torchx/pipelines/kfp/adapter.py,sha256=5GeHULjb1kxG6wJtYVLpNkgdzUi4iYEaR42VFOwT6fY,9045
|
|
57
57
|
torchx/pipelines/kfp/version.py,sha256=mYBxd6bm4MeR34D--xo-JLQ9wHeAl_ZQLwbItCf9tr0,539
|
|
58
58
|
torchx/runner/__init__.py,sha256=x8Sz7s_tLxPgJgvWIhK4ju9BNZU61uBFywGwDY6CqJs,315
|
|
59
|
-
torchx/runner/api.py,sha256=
|
|
60
|
-
torchx/runner/config.py,sha256=
|
|
59
|
+
torchx/runner/api.py,sha256=CJmTjoV2kB0FVqeE9B-bYaFyiMuQsZCY32kY13CIk6I,30559
|
|
60
|
+
torchx/runner/config.py,sha256=20X-vveAJVjb1AjjDSC6x_BVcdrTj9_ZLt_CHTykiFo,18266
|
|
61
61
|
torchx/runner/events/__init__.py,sha256=1_y0bojXl3FL0zlAj7BI4Dg5cXKXUmaa2jZbVH0EDUA,5268
|
|
62
62
|
torchx/runner/events/api.py,sha256=pPLfowWTXtN_XcrEDNI45pE6Ijvdc_Gdxq76RduqgGE,2664
|
|
63
63
|
torchx/runner/events/handlers.py,sha256=ThHCIJW21BfBgB7b6ftyjASJmD1KdizpjuTtsyqnvJs,522
|
|
@@ -82,9 +82,9 @@ torchx/schedulers/streams.py,sha256=8_SLezgnWgfv_zXUsJCUM34-h2dtv25NmZuxEwkzmxw,
|
|
|
82
82
|
torchx/schedulers/ray/__init__.py,sha256=fE0IHi1JJpxsNVBNzWNee2thrNXFFRhY94c80RxNSIE,231
|
|
83
83
|
torchx/schedulers/ray/ray_common.py,sha256=pyNYFvTKVwdjDAeCBNbPwAWwVNmlLOJWExfn90XY8u8,610
|
|
84
84
|
torchx/schedulers/ray/ray_driver.py,sha256=RdaCLfth16ky-5PDVOWRe_RuheWJu9xufWux2F9T7iw,12302
|
|
85
|
-
torchx/specs/__init__.py,sha256=
|
|
85
|
+
torchx/specs/__init__.py,sha256=Gw_2actqR_oWFtxEkGXCxGk_yrWK5JDZzwysyyqmXao,6438
|
|
86
86
|
torchx/specs/api.py,sha256=wkhHOxeWH_tFO3npKqPhNg4VX2NH5gPIFEylkPBo3AU,41315
|
|
87
|
-
torchx/specs/builders.py,sha256=
|
|
87
|
+
torchx/specs/builders.py,sha256=aozVl4q3h0mY5DDJCY1M1CyLC9SW66KJy8JIih8bZJo,13810
|
|
88
88
|
torchx/specs/file_linter.py,sha256=QCwob5STTBuy8RsxaevTI-Dk6R8siDJn81LyaOwazes,12333
|
|
89
89
|
torchx/specs/finder.py,sha256=lMCS1hUR-x7j4sAjfJZnKFeS1RlkFTeJtit4EL_ErIo,18182
|
|
90
90
|
torchx/specs/named_resources_aws.py,sha256=ISjHtifRJqB8u7PeAMiyLyO_S0WCaZiK-CFF3qe6JDU,11415
|
|
@@ -115,9 +115,9 @@ torchx/workspace/__init__.py,sha256=FqN8AN4VhR1C_SBY10MggQvNZmyanbbuPuE-JCjkyUY,
|
|
|
115
115
|
torchx/workspace/api.py,sha256=PtDkGTC5lX03pRoYpuMz2KCmM1ZOycRP1UknqvNb97Y,6341
|
|
116
116
|
torchx/workspace/dir_workspace.py,sha256=npNW_IjUZm_yS5r-8hrRkH46ndDd9a_eApT64m1S1T4,2268
|
|
117
117
|
torchx/workspace/docker_workspace.py,sha256=PFu2KQNVC-0p2aKJ-W_BKA9ZOmXdCY2ABEkCExp3udQ,10269
|
|
118
|
-
torchx_nightly-2025.9.
|
|
119
|
-
torchx_nightly-2025.9.
|
|
120
|
-
torchx_nightly-2025.9.
|
|
121
|
-
torchx_nightly-2025.9.
|
|
122
|
-
torchx_nightly-2025.9.
|
|
123
|
-
torchx_nightly-2025.9.
|
|
118
|
+
torchx_nightly-2025.9.5.dist-info/LICENSE,sha256=WVHfXhFC0Ia8LTKt_nJVYobdqTJVg_4J3Crrfm2A8KQ,1721
|
|
119
|
+
torchx_nightly-2025.9.5.dist-info/METADATA,sha256=Tldr_dcawwnBFNfDrRT1l1l9Ss_3TLBe2WM06JKMiik,6103
|
|
120
|
+
torchx_nightly-2025.9.5.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
121
|
+
torchx_nightly-2025.9.5.dist-info/entry_points.txt,sha256=T328AMXeKI3JZnnxfkEew2ZcMN1oQDtkXjMz7lkV-P4,169
|
|
122
|
+
torchx_nightly-2025.9.5.dist-info/top_level.txt,sha256=pxew3bc2gsiViS0zADs0jb6kC5v8o_Yy_85fhHj_J1A,7
|
|
123
|
+
torchx_nightly-2025.9.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|