devnomads-cli 0.3.0__tar.gz → 0.4.1__tar.gz
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.
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/PKG-INFO +14 -8
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/README.md +12 -4
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/devnomads_cli.egg-info/PKG-INFO +14 -8
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/devnomads_cli.egg-info/requires.txt +1 -4
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/dncli.py +38 -16
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/pyproject.toml +2 -5
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/tests/test_cert.py +52 -2
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/tests/test_config.py +2 -2
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/LICENSE +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/devnomads_cli.egg-info/SOURCES.txt +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/devnomads_cli.egg-info/dependency_links.txt +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/devnomads_cli.egg-info/entry_points.txt +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/devnomads_cli.egg-info/top_level.txt +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/setup.cfg +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/tests/test_cli.py +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/tests/test_generate.py +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/tests/test_generated_cli.py +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/tests/test_helpers.py +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/tests/test_hook.py +0 -0
- {devnomads_cli-0.3.0 → devnomads_cli-0.4.1}/tests/test_transfer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devnomads-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Manage your DevNomads services from the command line
|
|
5
5
|
Author-email: DevNomads <support@devnomads.nl>
|
|
6
6
|
License: MIT
|
|
@@ -11,9 +11,7 @@ Requires-Dist: typer>=0.12
|
|
|
11
11
|
Requires-Dist: httpx>=0.27
|
|
12
12
|
Requires-Dist: rich>=13
|
|
13
13
|
Requires-Dist: cryptography>=42
|
|
14
|
-
Requires-Dist: devnomads>=0.
|
|
15
|
-
Provides-Extra: cert
|
|
16
|
-
Requires-Dist: devnomads[acme]>=0.1; extra == "cert"
|
|
14
|
+
Requires-Dist: devnomads[acme]>=0.2.3
|
|
17
15
|
Dynamic: license-file
|
|
18
16
|
|
|
19
17
|
# dncli
|
|
@@ -56,7 +54,7 @@ Sleutels**, then store it:
|
|
|
56
54
|
dncli configure
|
|
57
55
|
```
|
|
58
56
|
|
|
59
|
-
The key is written to `~/.config/
|
|
57
|
+
The key is written to `~/.config/dncli/credentials`, readable only
|
|
60
58
|
by you. From here every command works:
|
|
61
59
|
|
|
62
60
|
```sh
|
|
@@ -128,13 +126,21 @@ dncli services list | jq -r '.[].entity'
|
|
|
128
126
|
|
|
129
127
|
## Certificates
|
|
130
128
|
|
|
131
|
-
|
|
129
|
+
`dncli` issues Let's Encrypt certificates over DNS-01 and HTTP-01:
|
|
132
130
|
|
|
133
131
|
```sh
|
|
134
|
-
|
|
135
|
-
dncli cert issue example.com -d "*.example.com"
|
|
132
|
+
dncli cert issue example.com -d www.example.com -d "*.example.com"
|
|
136
133
|
```
|
|
137
134
|
|
|
135
|
+
The first argument is the primary domain (the certificate CN); add
|
|
136
|
+
extra names (SANs) by repeating `--san`/`-d`. The certificate is
|
|
137
|
+
written to `~/.config/dncli/certs/<domain>/` as `cert.pem`,
|
|
138
|
+
`fullchain.pem`, `chain.pem`, and `privkey.pem` (0600); override the
|
|
139
|
+
location with `--out`.
|
|
140
|
+
|
|
141
|
+
Keys are ECDSA P-384 by default. Pick another with `--key-type`
|
|
142
|
+
(`ecdsa256`, `ecdsa384`, `ecdsa521`, `rsa2048`, `rsa4096`).
|
|
143
|
+
|
|
138
144
|
A dehydrated-compatible DNS-01 hook ships as `dncli-dns-hook`:
|
|
139
145
|
|
|
140
146
|
```sh
|
|
@@ -38,7 +38,7 @@ Sleutels**, then store it:
|
|
|
38
38
|
dncli configure
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
The key is written to `~/.config/
|
|
41
|
+
The key is written to `~/.config/dncli/credentials`, readable only
|
|
42
42
|
by you. From here every command works:
|
|
43
43
|
|
|
44
44
|
```sh
|
|
@@ -110,13 +110,21 @@ dncli services list | jq -r '.[].entity'
|
|
|
110
110
|
|
|
111
111
|
## Certificates
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
`dncli` issues Let's Encrypt certificates over DNS-01 and HTTP-01:
|
|
114
114
|
|
|
115
115
|
```sh
|
|
116
|
-
|
|
117
|
-
dncli cert issue example.com -d "*.example.com"
|
|
116
|
+
dncli cert issue example.com -d www.example.com -d "*.example.com"
|
|
118
117
|
```
|
|
119
118
|
|
|
119
|
+
The first argument is the primary domain (the certificate CN); add
|
|
120
|
+
extra names (SANs) by repeating `--san`/`-d`. The certificate is
|
|
121
|
+
written to `~/.config/dncli/certs/<domain>/` as `cert.pem`,
|
|
122
|
+
`fullchain.pem`, `chain.pem`, and `privkey.pem` (0600); override the
|
|
123
|
+
location with `--out`.
|
|
124
|
+
|
|
125
|
+
Keys are ECDSA P-384 by default. Pick another with `--key-type`
|
|
126
|
+
(`ecdsa256`, `ecdsa384`, `ecdsa521`, `rsa2048`, `rsa4096`).
|
|
127
|
+
|
|
120
128
|
A dehydrated-compatible DNS-01 hook ships as `dncli-dns-hook`:
|
|
121
129
|
|
|
122
130
|
```sh
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devnomads-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Manage your DevNomads services from the command line
|
|
5
5
|
Author-email: DevNomads <support@devnomads.nl>
|
|
6
6
|
License: MIT
|
|
@@ -11,9 +11,7 @@ Requires-Dist: typer>=0.12
|
|
|
11
11
|
Requires-Dist: httpx>=0.27
|
|
12
12
|
Requires-Dist: rich>=13
|
|
13
13
|
Requires-Dist: cryptography>=42
|
|
14
|
-
Requires-Dist: devnomads>=0.
|
|
15
|
-
Provides-Extra: cert
|
|
16
|
-
Requires-Dist: devnomads[acme]>=0.1; extra == "cert"
|
|
14
|
+
Requires-Dist: devnomads[acme]>=0.2.3
|
|
17
15
|
Dynamic: license-file
|
|
18
16
|
|
|
19
17
|
# dncli
|
|
@@ -56,7 +54,7 @@ Sleutels**, then store it:
|
|
|
56
54
|
dncli configure
|
|
57
55
|
```
|
|
58
56
|
|
|
59
|
-
The key is written to `~/.config/
|
|
57
|
+
The key is written to `~/.config/dncli/credentials`, readable only
|
|
60
58
|
by you. From here every command works:
|
|
61
59
|
|
|
62
60
|
```sh
|
|
@@ -128,13 +126,21 @@ dncli services list | jq -r '.[].entity'
|
|
|
128
126
|
|
|
129
127
|
## Certificates
|
|
130
128
|
|
|
131
|
-
|
|
129
|
+
`dncli` issues Let's Encrypt certificates over DNS-01 and HTTP-01:
|
|
132
130
|
|
|
133
131
|
```sh
|
|
134
|
-
|
|
135
|
-
dncli cert issue example.com -d "*.example.com"
|
|
132
|
+
dncli cert issue example.com -d www.example.com -d "*.example.com"
|
|
136
133
|
```
|
|
137
134
|
|
|
135
|
+
The first argument is the primary domain (the certificate CN); add
|
|
136
|
+
extra names (SANs) by repeating `--san`/`-d`. The certificate is
|
|
137
|
+
written to `~/.config/dncli/certs/<domain>/` as `cert.pem`,
|
|
138
|
+
`fullchain.pem`, `chain.pem`, and `privkey.pem` (0600); override the
|
|
139
|
+
location with `--out`.
|
|
140
|
+
|
|
141
|
+
Keys are ECDSA P-384 by default. Pick another with `--key-type`
|
|
142
|
+
(`ecdsa256`, `ecdsa384`, `ecdsa521`, `rsa2048`, `rsa4096`).
|
|
143
|
+
|
|
138
144
|
A dehydrated-compatible DNS-01 hook ships as `dncli-dns-hook`:
|
|
139
145
|
|
|
140
146
|
```sh
|
|
@@ -44,6 +44,7 @@ from devnomads.api import DevNomadsError
|
|
|
44
44
|
from devnomads.api.client import _unwrap as _lib_unwrap
|
|
45
45
|
from devnomads.dns import Dns, challenge_name
|
|
46
46
|
from rich.console import Console
|
|
47
|
+
from rich.markup import escape
|
|
47
48
|
from rich.table import Table
|
|
48
49
|
from typer.core import TyperGroup
|
|
49
50
|
|
|
@@ -71,7 +72,7 @@ class CliError(typer.Exit):
|
|
|
71
72
|
|
|
72
73
|
def __init__(self, message: str) -> None:
|
|
73
74
|
# soft_wrap keeps the error a single grep-able line
|
|
74
|
-
err_console.print(f"[red]error:[/] {message}", soft_wrap=True)
|
|
75
|
+
err_console.print(f"[red]error:[/] {escape(message)}", soft_wrap=True)
|
|
75
76
|
self.message = message
|
|
76
77
|
super().__init__(code=1)
|
|
77
78
|
|
|
@@ -85,7 +86,7 @@ def config_dir() -> Path:
|
|
|
85
86
|
return Path(env_dir)
|
|
86
87
|
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
87
88
|
base = Path(xdg) if xdg else Path.home() / ".config"
|
|
88
|
-
return base / "
|
|
89
|
+
return base / "dncli"
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
def credentials_path() -> Path:
|
|
@@ -1275,10 +1276,12 @@ def _hook_dispatch(dns: Dns, operation: str, args: list[str]) -> int:
|
|
|
1275
1276
|
else:
|
|
1276
1277
|
dns.unset_txt(record, token_value)
|
|
1277
1278
|
except (AuthError, ApiError) as exc:
|
|
1278
|
-
err_console.print(
|
|
1279
|
+
err_console.print(
|
|
1280
|
+
f"[red]error:[/] {escape(_error_message(exc.status, exc.detail))}"
|
|
1281
|
+
)
|
|
1279
1282
|
return 1
|
|
1280
1283
|
except DevNomadsError as exc:
|
|
1281
|
-
err_console.print(f"[red]error:[/] {exc}")
|
|
1284
|
+
err_console.print(f"[red]error:[/] {escape(str(exc))}")
|
|
1282
1285
|
return 1
|
|
1283
1286
|
return 0
|
|
1284
1287
|
|
|
@@ -1306,7 +1309,7 @@ def hook_main(argv: list[str] | None = None) -> int:
|
|
|
1306
1309
|
try:
|
|
1307
1310
|
client = ApiClient.from_environment()
|
|
1308
1311
|
except DevNomadsError as exc:
|
|
1309
|
-
err_console.print(f"[red]error:[/] {exc}")
|
|
1312
|
+
err_console.print(f"[red]error:[/] {escape(str(exc))}")
|
|
1310
1313
|
return 1
|
|
1311
1314
|
try:
|
|
1312
1315
|
return _hook_dispatch(Dns(client), operation, argv[2:])
|
|
@@ -1359,17 +1362,26 @@ LE_STAGING_DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
|
1359
1362
|
CERT_RENEW_WINDOW_DAYS = 30
|
|
1360
1363
|
|
|
1361
1364
|
|
|
1365
|
+
class KeyType(str, Enum):
|
|
1366
|
+
"""Certificate key types. ECDSA is the default; RSA on request."""
|
|
1367
|
+
|
|
1368
|
+
ecdsa256 = "ecdsa256"
|
|
1369
|
+
ecdsa384 = "ecdsa384"
|
|
1370
|
+
ecdsa521 = "ecdsa521"
|
|
1371
|
+
rsa2048 = "rsa2048"
|
|
1372
|
+
rsa4096 = "rsa4096"
|
|
1373
|
+
|
|
1374
|
+
|
|
1362
1375
|
def _load_acme() -> Any:
|
|
1363
|
-
"""Import devnomads.acme lazily,
|
|
1364
|
-
|
|
1376
|
+
"""Import devnomads.acme lazily, so non-cert commands skip the ACME
|
|
1377
|
+
stack at startup."""
|
|
1365
1378
|
|
|
1366
1379
|
try:
|
|
1367
1380
|
import devnomads.acme as acme
|
|
1368
1381
|
except ImportError as exc:
|
|
1369
1382
|
raise CliError(
|
|
1370
|
-
"
|
|
1371
|
-
'pip install "devnomads-cli
|
|
1372
|
-
'(or: uv tool install "devnomads-cli[cert]")'
|
|
1383
|
+
"could not import devnomads.acme; the install looks broken - "
|
|
1384
|
+
'reinstall with: pip install --force-reinstall "devnomads-cli"'
|
|
1373
1385
|
) from exc
|
|
1374
1386
|
return acme
|
|
1375
1387
|
|
|
@@ -1496,8 +1508,13 @@ def cert_issue(
|
|
|
1496
1508
|
str | None, typer.Option("--email", help="ACME account contact email.")
|
|
1497
1509
|
] = None,
|
|
1498
1510
|
key_type: Annotated[
|
|
1499
|
-
|
|
1500
|
-
|
|
1511
|
+
KeyType,
|
|
1512
|
+
typer.Option(
|
|
1513
|
+
"--key-type",
|
|
1514
|
+
help="Certificate key type "
|
|
1515
|
+
"(ecdsa256/ecdsa384/ecdsa521/rsa2048/rsa4096).",
|
|
1516
|
+
),
|
|
1517
|
+
] = KeyType.ecdsa384,
|
|
1501
1518
|
staging: Annotated[
|
|
1502
1519
|
bool, typer.Option("--staging", help="Use the Let's Encrypt staging CA.")
|
|
1503
1520
|
] = False,
|
|
@@ -1526,7 +1543,7 @@ def cert_issue(
|
|
|
1526
1543
|
webroot=webroot,
|
|
1527
1544
|
standalone=standalone,
|
|
1528
1545
|
email=email,
|
|
1529
|
-
key_type=key_type,
|
|
1546
|
+
key_type=key_type.value,
|
|
1530
1547
|
staging=staging,
|
|
1531
1548
|
out_dir=_cert_out_dir(out, domain),
|
|
1532
1549
|
)
|
|
@@ -1562,8 +1579,13 @@ def cert_renew(
|
|
|
1562
1579
|
str | None, typer.Option("--email", help="ACME account contact email.")
|
|
1563
1580
|
] = None,
|
|
1564
1581
|
key_type: Annotated[
|
|
1565
|
-
|
|
1566
|
-
|
|
1582
|
+
KeyType,
|
|
1583
|
+
typer.Option(
|
|
1584
|
+
"--key-type",
|
|
1585
|
+
help="Certificate key type "
|
|
1586
|
+
"(ecdsa256/ecdsa384/ecdsa521/rsa2048/rsa4096).",
|
|
1587
|
+
),
|
|
1588
|
+
] = KeyType.ecdsa384,
|
|
1567
1589
|
staging: Annotated[
|
|
1568
1590
|
bool, typer.Option("--staging", help="Use the Let's Encrypt staging CA.")
|
|
1569
1591
|
] = False,
|
|
@@ -1595,7 +1617,7 @@ def cert_renew(
|
|
|
1595
1617
|
webroot=None,
|
|
1596
1618
|
standalone=False,
|
|
1597
1619
|
email=email,
|
|
1598
|
-
key_type=key_type,
|
|
1620
|
+
key_type=key_type.value,
|
|
1599
1621
|
staging=staging,
|
|
1600
1622
|
out_dir=out_dir,
|
|
1601
1623
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "devnomads-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.1"
|
|
4
4
|
description = "Manage your DevNomads services from the command line"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -11,12 +11,9 @@ dependencies = [
|
|
|
11
11
|
"httpx>=0.27",
|
|
12
12
|
"rich>=13",
|
|
13
13
|
"cryptography>=42",
|
|
14
|
-
"devnomads>=0.
|
|
14
|
+
"devnomads[acme]>=0.2.3",
|
|
15
15
|
]
|
|
16
16
|
|
|
17
|
-
[project.optional-dependencies]
|
|
18
|
-
cert = ["devnomads[acme]>=0.1"]
|
|
19
|
-
|
|
20
17
|
[project.scripts]
|
|
21
18
|
dncli = "dncli:run"
|
|
22
19
|
dncli-dns-hook = "dncli:hook_main"
|
|
@@ -57,6 +57,56 @@ def test_issue_dns01_default_uses_dns_provider(configured, captured, isolated_co
|
|
|
57
57
|
assert stat.S_IMODE(privkey.stat().st_mode) == 0o600
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def _spy_generate_key(monkeypatch, seen):
|
|
61
|
+
import devnomads.acme as acme
|
|
62
|
+
|
|
63
|
+
def spy(algo):
|
|
64
|
+
seen["algo"] = algo
|
|
65
|
+
return object()
|
|
66
|
+
|
|
67
|
+
monkeypatch.setattr(acme, "generate_key", spy)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_issue_default_key_type_is_ecdsa384(
|
|
71
|
+
configured, captured, isolated_config, monkeypatch
|
|
72
|
+
):
|
|
73
|
+
seen = {}
|
|
74
|
+
_spy_generate_key(monkeypatch, seen)
|
|
75
|
+
result = runner.invoke(app, ["cert", "issue", "example.com"])
|
|
76
|
+
assert result.exit_code == 0, result.output
|
|
77
|
+
assert seen["algo"] == "ecdsa384"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_issue_key_type_ecdsa521(configured, captured, isolated_config, monkeypatch):
|
|
81
|
+
seen = {}
|
|
82
|
+
_spy_generate_key(monkeypatch, seen)
|
|
83
|
+
result = runner.invoke(
|
|
84
|
+
app, ["cert", "issue", "example.com", "--key-type", "ecdsa521"]
|
|
85
|
+
)
|
|
86
|
+
assert result.exit_code == 0, result.output
|
|
87
|
+
assert seen["algo"] == "ecdsa521"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_issue_key_type_is_selectable(
|
|
91
|
+
configured, captured, isolated_config, monkeypatch
|
|
92
|
+
):
|
|
93
|
+
seen = {}
|
|
94
|
+
_spy_generate_key(monkeypatch, seen)
|
|
95
|
+
result = runner.invoke(
|
|
96
|
+
app, ["cert", "issue", "example.com", "--key-type", "rsa4096"]
|
|
97
|
+
)
|
|
98
|
+
assert result.exit_code == 0, result.output
|
|
99
|
+
assert seen["algo"] == "rsa4096"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_issue_rejects_unknown_key_type():
|
|
103
|
+
result = runner.invoke(
|
|
104
|
+
app, ["cert", "issue", "example.com", "--key-type", "ed25519"]
|
|
105
|
+
)
|
|
106
|
+
assert result.exit_code != 0
|
|
107
|
+
assert "ed25519" in result.output
|
|
108
|
+
|
|
109
|
+
|
|
60
110
|
def test_issue_http01_webroot_uses_webroot_solver(
|
|
61
111
|
configured, captured, isolated_config, tmp_path
|
|
62
112
|
):
|
|
@@ -119,7 +169,7 @@ def test_issue_out_dir_override(configured, captured, isolated_config, tmp_path)
|
|
|
119
169
|
assert (out / "fullchain.pem").read_text() == FULLCHAIN
|
|
120
170
|
|
|
121
171
|
|
|
122
|
-
def
|
|
172
|
+
def test_load_acme_import_error_message(monkeypatch):
|
|
123
173
|
import builtins
|
|
124
174
|
|
|
125
175
|
real_import = builtins.__import__
|
|
@@ -132,7 +182,7 @@ def test_load_acme_missing_extra_message(monkeypatch):
|
|
|
132
182
|
monkeypatch.setattr(builtins, "__import__", fake_import)
|
|
133
183
|
result = runner.invoke(app, ["cert", "issue", "example.com"])
|
|
134
184
|
assert result.exit_code == 1
|
|
135
|
-
assert "
|
|
185
|
+
assert "devnomads.acme" in result.output
|
|
136
186
|
|
|
137
187
|
|
|
138
188
|
def test_renew_skips_valid_cert(configured, captured, isolated_config):
|
|
@@ -16,14 +16,14 @@ def test_config_dir_from_env(isolated_config):
|
|
|
16
16
|
def test_config_dir_xdg_fallback(monkeypatch, tmp_path):
|
|
17
17
|
monkeypatch.delenv(dncli.ENV_CONFIG_DIR, raising=False)
|
|
18
18
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
|
|
19
|
-
assert dncli.config_dir() == tmp_path / "xdg" / "
|
|
19
|
+
assert dncli.config_dir() == tmp_path / "xdg" / "dncli"
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def test_config_dir_home_fallback(monkeypatch, tmp_path):
|
|
23
23
|
monkeypatch.delenv(dncli.ENV_CONFIG_DIR, raising=False)
|
|
24
24
|
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
|
|
25
25
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
26
|
-
assert dncli.config_dir() == tmp_path / ".config" / "
|
|
26
|
+
assert dncli.config_dir() == tmp_path / ".config" / "dncli"
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def test_save_credentials_sets_strict_permissions(write_profile):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|