humancli 0.1.0__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.
- agentcli/__init__.py +20 -0
- agentcli/_agents.py +93 -0
- agentcli/_app.py +432 -0
- agentcli/_context.py +39 -0
- agentcli/_errors.py +140 -0
- agentcli/_help.py +65 -0
- agentcli/_output.py +282 -0
- agentcli/_parser.py +417 -0
- agentcli/_schema.py +142 -0
- agentcli/_types.py +142 -0
- agentcli/_wizard.py +83 -0
- agentcli/prompt.py +71 -0
- agentcli/py.typed +0 -0
- agentcli/testing.py +55 -0
- humancli-0.1.0.dist-info/METADATA +178 -0
- humancli-0.1.0.dist-info/RECORD +18 -0
- humancli-0.1.0.dist-info/WHEEL +4 -0
- humancli-0.1.0.dist-info/licenses/LICENSE +21 -0
agentcli/_wizard.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Literal, get_origin
|
|
6
|
+
|
|
7
|
+
from . import prompt as prompt_api
|
|
8
|
+
from ._types import CommandSchema, MISSING, ParameterSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def wizard_schema(command: CommandSchema) -> dict[str, Any]:
|
|
12
|
+
return {
|
|
13
|
+
"command": command.command,
|
|
14
|
+
"description": command.description,
|
|
15
|
+
"parameters": [
|
|
16
|
+
serialize_parameter(parameter)
|
|
17
|
+
for parameter in command.all_parameters
|
|
18
|
+
if not parameter.hidden
|
|
19
|
+
],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def prompt_for_parameter(
|
|
24
|
+
parameter: ParameterSpec, *, stdin: Any = None, stdout: Any = None
|
|
25
|
+
) -> Any:
|
|
26
|
+
message = parameter.prompt or parameter.description or parameter.name
|
|
27
|
+
if parameter.is_bool:
|
|
28
|
+
default = (
|
|
29
|
+
bool(parameter.default)
|
|
30
|
+
if parameter.has_default and parameter.default is not None
|
|
31
|
+
else False
|
|
32
|
+
)
|
|
33
|
+
return prompt_api.confirm(message, default=default, stdin=stdin, stdout=stdout)
|
|
34
|
+
if parameter.choices:
|
|
35
|
+
return prompt_api.select(
|
|
36
|
+
message,
|
|
37
|
+
parameter.choices,
|
|
38
|
+
default=_default(parameter.default),
|
|
39
|
+
stdin=stdin,
|
|
40
|
+
stdout=stdout,
|
|
41
|
+
)
|
|
42
|
+
if parameter.secret:
|
|
43
|
+
return prompt_api.password(
|
|
44
|
+
message, default=_default(parameter.default), stdin=stdin, stdout=stdout
|
|
45
|
+
)
|
|
46
|
+
if parameter.is_list:
|
|
47
|
+
raw = prompt_api.text(message, default="", stdin=stdin, stdout=stdout)
|
|
48
|
+
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
49
|
+
return prompt_api.text(
|
|
50
|
+
message, default=_default(parameter.default), stdin=stdin, stdout=stdout
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def serialize_parameter(parameter: ParameterSpec) -> dict[str, Any]:
|
|
55
|
+
return {
|
|
56
|
+
"name": parameter.name,
|
|
57
|
+
"kind": parameter.kind,
|
|
58
|
+
"flag": None if parameter.kind == "argument" else f"--{parameter.cli_name}",
|
|
59
|
+
"required": parameter.required,
|
|
60
|
+
"description": parameter.description,
|
|
61
|
+
"choices": list(parameter.choices) if parameter.choices else None,
|
|
62
|
+
"default": None
|
|
63
|
+
if parameter.default in (MISSING, inspect.Signature.empty)
|
|
64
|
+
else parameter.default,
|
|
65
|
+
"type": parameter_type(parameter.annotation),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parameter_type(annotation: Any) -> str:
|
|
70
|
+
origin = get_origin(annotation)
|
|
71
|
+
if origin is Literal:
|
|
72
|
+
return "literal"
|
|
73
|
+
if origin in (list, tuple):
|
|
74
|
+
return "list"
|
|
75
|
+
if inspect.isclass(annotation) and issubclass(annotation, Enum):
|
|
76
|
+
return "enum"
|
|
77
|
+
return getattr(annotation, "__name__", str(annotation))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _default(value: Any) -> str | None:
|
|
81
|
+
if value in (None, MISSING, inspect.Signature.empty):
|
|
82
|
+
return None
|
|
83
|
+
return str(value)
|
agentcli/prompt.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def text(
|
|
9
|
+
message: str,
|
|
10
|
+
*,
|
|
11
|
+
default: Any = None,
|
|
12
|
+
stdin: Any = None,
|
|
13
|
+
stdout: Any = None,
|
|
14
|
+
) -> str:
|
|
15
|
+
input_stream = stdin or sys.stdin
|
|
16
|
+
output_stream = stdout or sys.stdout
|
|
17
|
+
suffix = f" [{default}]" if default not in (None, "") else ""
|
|
18
|
+
output_stream.write(f"? {message}{suffix}: ")
|
|
19
|
+
output_stream.flush()
|
|
20
|
+
value = input_stream.readline().rstrip("\n")
|
|
21
|
+
return str(default) if value == "" and default is not None else value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def password(message: str, *, stdin: Any = None, stdout: Any = None) -> str:
|
|
25
|
+
input_stream = stdin or sys.stdin
|
|
26
|
+
output_stream = stdout or sys.stdout
|
|
27
|
+
if input_stream is sys.stdin and output_stream is sys.stdout:
|
|
28
|
+
return getpass.getpass(f"? {message}: ")
|
|
29
|
+
output_stream.write(f"? {message}: ")
|
|
30
|
+
output_stream.flush()
|
|
31
|
+
return input_stream.readline().rstrip("\n")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def confirm(
|
|
35
|
+
message: str,
|
|
36
|
+
*,
|
|
37
|
+
default: bool = False,
|
|
38
|
+
stdin: Any = None,
|
|
39
|
+
stdout: Any = None,
|
|
40
|
+
) -> bool:
|
|
41
|
+
answer = (
|
|
42
|
+
text(
|
|
43
|
+
f"{message} [{'Y/n' if default else 'y/N'}]",
|
|
44
|
+
default="y" if default else "n",
|
|
45
|
+
stdin=stdin,
|
|
46
|
+
stdout=stdout,
|
|
47
|
+
)
|
|
48
|
+
.strip()
|
|
49
|
+
.lower()
|
|
50
|
+
)
|
|
51
|
+
return answer in {"y", "yes", "true", "1"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def select(
|
|
55
|
+
message: str,
|
|
56
|
+
options: list[str],
|
|
57
|
+
*,
|
|
58
|
+
stdin: Any = None,
|
|
59
|
+
stdout: Any = None,
|
|
60
|
+
) -> str:
|
|
61
|
+
output_stream = stdout or sys.stdout
|
|
62
|
+
output_stream.write(f"? {message}: {', '.join(options)}\n")
|
|
63
|
+
output_stream.flush()
|
|
64
|
+
choice = text("Choice", stdin=stdin, stdout=stdout)
|
|
65
|
+
if choice in options:
|
|
66
|
+
return choice
|
|
67
|
+
if choice.isdigit():
|
|
68
|
+
index = int(choice) - 1
|
|
69
|
+
if 0 <= index < len(options):
|
|
70
|
+
return options[index]
|
|
71
|
+
raise ValueError(f"Invalid selection: {choice}")
|
agentcli/py.typed
ADDED
|
File without changes
|
agentcli/testing.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class CliResult:
|
|
11
|
+
exit_code: int
|
|
12
|
+
output: str
|
|
13
|
+
data: Any = None
|
|
14
|
+
error: Any = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CliRunner:
|
|
18
|
+
def __init__(self, app: Any) -> None:
|
|
19
|
+
self.app = app
|
|
20
|
+
|
|
21
|
+
def invoke(
|
|
22
|
+
self,
|
|
23
|
+
argv: list[str],
|
|
24
|
+
*,
|
|
25
|
+
env: Mapping[str, str] | None = None,
|
|
26
|
+
is_tty: bool = False,
|
|
27
|
+
stdin: str = "",
|
|
28
|
+
) -> CliResult:
|
|
29
|
+
stdout = io.StringIO()
|
|
30
|
+
input_stream = io.StringIO(stdin)
|
|
31
|
+
result = self.app.invoke(
|
|
32
|
+
argv=argv,
|
|
33
|
+
env=dict(env or {}),
|
|
34
|
+
is_tty=is_tty,
|
|
35
|
+
stdin=input_stream,
|
|
36
|
+
stdout=stdout,
|
|
37
|
+
)
|
|
38
|
+
output = stdout.getvalue()
|
|
39
|
+
data = result.data
|
|
40
|
+
error = result.error
|
|
41
|
+
try:
|
|
42
|
+
parsed = json.loads(output)
|
|
43
|
+
except Exception:
|
|
44
|
+
parsed = None
|
|
45
|
+
if isinstance(parsed, dict) and "ok" in parsed:
|
|
46
|
+
data = parsed.get("data")
|
|
47
|
+
error = parsed.get("error")
|
|
48
|
+
elif parsed is not None:
|
|
49
|
+
data = parsed
|
|
50
|
+
return CliResult(
|
|
51
|
+
exit_code=result.exit_code,
|
|
52
|
+
output=output,
|
|
53
|
+
data=data,
|
|
54
|
+
error=error,
|
|
55
|
+
)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: humancli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python CLIs for agents and humans
|
|
5
|
+
Project-URL: Repository, https://github.com/elyase/agentcli
|
|
6
|
+
Project-URL: Issues, https://github.com/elyase/agentcli/issues
|
|
7
|
+
Author-email: Yaser Martinez Palenzuela <yaser.martinez@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,ai,cli,command-line,llm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: docstring-parser>=0.16
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: mcp>=1.0; extra == 'all'
|
|
25
|
+
Requires-Dist: pydantic>=2.0; extra == 'all'
|
|
26
|
+
Requires-Dist: pyyaml>=6.0; extra == 'all'
|
|
27
|
+
Requires-Dist: rich>=13.0; extra == 'all'
|
|
28
|
+
Provides-Extra: mcp
|
|
29
|
+
Requires-Dist: mcp>=1.0; extra == 'mcp'
|
|
30
|
+
Provides-Extra: pydantic
|
|
31
|
+
Requires-Dist: pydantic>=2.0; extra == 'pydantic'
|
|
32
|
+
Provides-Extra: rich
|
|
33
|
+
Requires-Dist: rich>=13.0; extra == 'rich'
|
|
34
|
+
Provides-Extra: yaml
|
|
35
|
+
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# agentcli
|
|
39
|
+
|
|
40
|
+
Python CLIs for agents and humans.
|
|
41
|
+
|
|
42
|
+
agentcli builds command-line interfaces that produce structured, parseable output for AI agents while remaining human-friendly. Type hints are the schema — a function signature IS the CLI specification.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pip install humancli
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The package installs as `humancli` on PyPI but you import it as `agentcli`:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from agentcli import App
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
### Single function
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from agentcli import run
|
|
62
|
+
|
|
63
|
+
def greet(name: str):
|
|
64
|
+
"""Greet someone."""
|
|
65
|
+
return {"message": f"hello {name}"}
|
|
66
|
+
|
|
67
|
+
run(greet)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
$ greet world
|
|
72
|
+
message: hello world
|
|
73
|
+
|
|
74
|
+
$ greet world --json
|
|
75
|
+
{"ok": true, "data": {"message": "hello world"}}
|
|
76
|
+
|
|
77
|
+
$ greet --llms
|
|
78
|
+
# greet
|
|
79
|
+
| Command | Description |
|
|
80
|
+
|---------|-------------|
|
|
81
|
+
| `greet <name>` | Greet someone |
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Multi-command app
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from agentcli import App
|
|
88
|
+
|
|
89
|
+
app = App("my-cli", version="1.0.0")
|
|
90
|
+
|
|
91
|
+
@app.command
|
|
92
|
+
def status():
|
|
93
|
+
"""Show status."""
|
|
94
|
+
return {"clean": True, "branch": "main"}
|
|
95
|
+
|
|
96
|
+
@app.command
|
|
97
|
+
def install(package: str, *, save_dev: bool = False):
|
|
98
|
+
"""Install a package."""
|
|
99
|
+
return {"added": 1, "packages": 451}
|
|
100
|
+
|
|
101
|
+
app()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Parameters before `*` are positional arguments. Parameters after `*` are named options/flags. This is just Python's own syntax.
|
|
105
|
+
|
|
106
|
+
### Parameter metadata
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from typing import Annotated, Literal
|
|
110
|
+
from agentcli import App, Param
|
|
111
|
+
|
|
112
|
+
app = App("deploy-cli")
|
|
113
|
+
|
|
114
|
+
@app.command
|
|
115
|
+
def deploy(
|
|
116
|
+
env: Annotated[Literal["staging", "prod"], Param(help="Target environment")],
|
|
117
|
+
*,
|
|
118
|
+
token: Annotated[str, Param(env="DEPLOY_TOKEN", secret=True)] = "",
|
|
119
|
+
):
|
|
120
|
+
"""Deploy to an environment."""
|
|
121
|
+
return {"url": f"https://{env}.example.com"}
|
|
122
|
+
|
|
123
|
+
app()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Sub-apps
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
app = App("gh")
|
|
130
|
+
pr = App("pr")
|
|
131
|
+
|
|
132
|
+
@pr.command
|
|
133
|
+
def list_(*, state: Literal["open", "closed"] = "open"):
|
|
134
|
+
"""List pull requests."""
|
|
135
|
+
return {"prs": [], "state": state}
|
|
136
|
+
|
|
137
|
+
app.mount(pr)
|
|
138
|
+
app()
|
|
139
|
+
|
|
140
|
+
# $ gh pr list --state closed
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Default commands
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
app = App("fetch")
|
|
147
|
+
|
|
148
|
+
@app.default
|
|
149
|
+
def fetch_cases(*, limit: int = 20):
|
|
150
|
+
"""Fetch cases."""
|
|
151
|
+
return {"fetched": limit}
|
|
152
|
+
|
|
153
|
+
# Runs when no sub-command is given:
|
|
154
|
+
# $ fetch --limit 5
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Agent discovery
|
|
158
|
+
|
|
159
|
+
Every agentcli app gets built-in flags for agent consumption:
|
|
160
|
+
|
|
161
|
+
- `--llms` — markdown command index
|
|
162
|
+
- `--llms-full` — full JSON schema of all commands
|
|
163
|
+
- `--json` / `--yaml` / `--jsonl` — structured output formats
|
|
164
|
+
- `--mcp` — start as an MCP server (requires `agentcli[mcp]`)
|
|
165
|
+
|
|
166
|
+
## Optional extras
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
pip install humancli[rich] # rich terminal formatting
|
|
170
|
+
pip install humancli[pydantic] # pydantic model support
|
|
171
|
+
pip install humancli[yaml] # yaml output format
|
|
172
|
+
pip install humancli[mcp] # MCP server mode
|
|
173
|
+
pip install humancli[all] # everything
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
agentcli/__init__.py,sha256=HxjImv5iEFR6gnh5QyoEne6sGKbiRevyWBd40z1aufA,403
|
|
2
|
+
agentcli/_agents.py,sha256=17m1RXYH2eAKioNrT9Gj8ahQjXZc7gyOzrGn_MPrlUE,3095
|
|
3
|
+
agentcli/_app.py,sha256=298niScGWCQ4GHw4y7v5Jfx2oI3xnaWJ6JfMLHvtpsw,15006
|
|
4
|
+
agentcli/_context.py,sha256=SqMmYndkVHLVKIDyH64Jes3GZfmlVASY3EtO18PGbRY,1161
|
|
5
|
+
agentcli/_errors.py,sha256=cEmYSCs76Agp-NSqEI7UFdij9HYjMWPInEyQvYoHfFo,3564
|
|
6
|
+
agentcli/_help.py,sha256=HhlN__lKUSB9T4VV_EGPxtsRnF8CrFJawRm7ttefjB0,2214
|
|
7
|
+
agentcli/_output.py,sha256=8FLwRjeQjHtZ2dRUcP8WS64w6bv_Fqdg0_ioUiYPqek,8371
|
|
8
|
+
agentcli/_parser.py,sha256=zuz2r0Bed0ro3rwOZPAaf4CJm2jYNjmMMHLnwAUtLhA,13822
|
|
9
|
+
agentcli/_schema.py,sha256=nPzBZHB0TuML1i397OyKcMz1jEiMFhwLCqJf5dNiv9M,4708
|
|
10
|
+
agentcli/_types.py,sha256=OzBDnAu8eyR6Uw3tAVV2UNKdtmUyky20eg1MYC5K7sk,3126
|
|
11
|
+
agentcli/_wizard.py,sha256=WRML8rKLHfOjw50xW9j6ZPNx_LaGK-a-3ergnjolCp4,2725
|
|
12
|
+
agentcli/prompt.py,sha256=AGBBSeGbjY7qiIxUMXledz_vLpnkwdb22RwBuvUY2T4,1880
|
|
13
|
+
agentcli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
agentcli/testing.py,sha256=wm6oZeYRhbHx-Da0sIvmtKl6AB_LZgOGhrh-4LLaiw0,1303
|
|
15
|
+
humancli-0.1.0.dist-info/METADATA,sha256=vR0V8-81rzlduSyUcK38lWptC9GNRs87YKyZmRwLGW8,4210
|
|
16
|
+
humancli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
17
|
+
humancli-0.1.0.dist-info/licenses/LICENSE,sha256=71w9pR4vtroGYKlGefIRpDZmUXAWVZIyyJpljlFZOzo,1082
|
|
18
|
+
humancli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yaser Martinez Palenzuela
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|