Skip to content

Commit 41ea492

Browse files
Merge pull request #2621 from fetchai/feature/check_for_dependecnies_on_push
check dependencies present in registry on package push
2 parents 945b45f + 154b8b0 commit 41ea492

File tree

5 files changed

+128
-26
lines changed

5 files changed

+128
-26
lines changed

Diff for: aea/cli/registry/push.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@
2121
import os
2222
import shutil
2323
import tarfile
24-
from typing import cast
24+
from typing import List, Tuple, cast
2525

2626
import click
2727

2828
from aea.cli.registry.utils import (
2929
check_is_author_logged_in,
3030
clean_tarfiles,
31+
list_missing_packages,
3132
request_api,
3233
)
3334
from aea.cli.utils.context import Context
@@ -39,6 +40,7 @@
3940
CONNECTIONS,
4041
CONTRACTS,
4142
DEFAULT_README_FILE,
43+
ITEM_TYPE_PLURAL_TO_TYPE,
4244
PROTOCOLS,
4345
SKILLS,
4446
)
@@ -143,11 +145,20 @@ def push_item(ctx: Context, item_type: str, item_id: PublicId) -> None:
143145
}
144146

145147
# dependencies
148+
dependencies: List[Tuple[str, PublicId]] = []
146149
for key in [CONNECTIONS, CONTRACTS, PROTOCOLS, SKILLS]:
147-
deps_list = item_config.get(key)
150+
deps_list = item_config.get(key, [])
148151
if deps_list:
149152
data.update({key: deps_list})
153+
for dep in deps_list:
154+
dependencies.append((ITEM_TYPE_PLURAL_TO_TYPE[key], PublicId.from_str(dep)))
150155

156+
missing_dependencies = list_missing_packages(dependencies)
157+
158+
if missing_dependencies:
159+
for package_type, package_id in missing_dependencies:
160+
click.echo(f"Error: Cannot find {package_type} {package_id} in registry!")
161+
raise click.ClickException("Found missing dependencies! Push canceled!")
151162
try:
152163
files = {"file": open(output_filepath, "rb")}
153164
readme_path = os.path.join(item_path, DEFAULT_README_FILE)
@@ -159,11 +170,11 @@ def push_item(ctx: Context, item_type: str, item_id: PublicId) -> None:
159170
resp = cast(
160171
JSONLike, request_api("POST", path, data=data, is_auth=True, files=files)
161172
)
173+
click.echo(
174+
"Successfully pushed {} {} to the Registry. Public ID: {}".format(
175+
item_type, item_id.name, resp["public_id"]
176+
)
177+
)
162178
finally:
163179
for fd in files.values():
164180
fd.close()
165-
click.echo(
166-
"Successfully pushed {} {} to the Registry. Public ID: {}".format(
167-
item_type, item_id.name, resp["public_id"]
168-
)
169-
)

Diff for: aea/cli/registry/utils.py

+43-14
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@
1717
#
1818
# ------------------------------------------------------------------------------
1919
"""Utils used for operating Registry with CLI."""
20-
2120
import os
2221
import tarfile
23-
from json.decoder import JSONDecodeError
24-
from typing import Any, Callable, Dict, Optional, Tuple, Union, cast
22+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
2523

2624
import click
2725

@@ -32,6 +30,7 @@
3230
from aea.cli.utils.package_utils import find_item_locally
3331
from aea.common import JSONLike
3432
from aea.configurations.base import PublicId
33+
from aea.configurations.constants import ITEM_TYPE_TO_PLURAL
3534
from aea.helpers import http_requests as requests
3635

3736

@@ -83,21 +82,12 @@ def request_api(
8382
'Please sign in with "aea login" command.'
8483
)
8584
headers.update({"Authorization": "Token {}".format(token)})
86-
87-
request_kwargs = dict(
88-
method=method,
89-
url="{}{}".format(REGISTRY_API_URL, path),
90-
params=params,
91-
files=files,
92-
data=data,
93-
headers=headers,
94-
)
9585
try:
96-
resp = requests.request(**request_kwargs)
86+
resp = _perform_registry_request(method, path, params, data, files, headers)
9787
resp_json = resp.json()
9888
except requests.exceptions.ConnectionError:
9989
raise click.ClickException("Registry server is not responding.")
100-
except JSONDecodeError:
90+
except requests.JSONDecodeError:
10191
resp_json = None
10292

10393
if resp.status_code == 200:
@@ -139,6 +129,27 @@ def request_api(
139129
return resp_json
140130

141131

132+
def _perform_registry_request(
133+
method: str,
134+
path: str,
135+
params: Optional[Dict] = None,
136+
data: Optional[Dict] = None,
137+
files: Optional[Dict] = None,
138+
headers: Optional[Dict] = None,
139+
) -> requests.Response:
140+
"""Perform HTTP request and resturn response object."""
141+
request_kwargs = dict(
142+
method=method,
143+
url="{}{}".format(REGISTRY_API_URL, path),
144+
params=params,
145+
files=files,
146+
data=data,
147+
headers=headers,
148+
)
149+
resp = requests.request(**request_kwargs)
150+
return resp
151+
152+
142153
def download_file(url: str, cwd: str, timeout: float = FILE_DOWNLOAD_TIMEOUT) -> str:
143154
"""
144155
Download file from URL and save it in CWD (current working directory).
@@ -322,3 +333,21 @@ def get_latest_version_available_in_registry(
322333
)
323334

324335
return latest_item_public_id
336+
337+
338+
def list_missing_packages(
339+
packages: List[Tuple[str, PublicId]]
340+
) -> List[Tuple[str, PublicId]]:
341+
"""Get list of packages not currently present in registry."""
342+
result: List[Tuple[str, PublicId]] = []
343+
344+
for package_type, package_id in packages:
345+
api_path = f"/{ITEM_TYPE_TO_PLURAL[package_type]}/{package_id.author}/{package_id.name}/{package_id.version}"
346+
resp = _perform_registry_request("GET", api_path)
347+
if resp.status_code == 404:
348+
result.append((package_type, package_id))
349+
elif resp.status_code == 200:
350+
pass
351+
else: # pragma: nocover
352+
raise ValueError("Error on registry request")
353+
return result

Diff for: aea/helpers/http_requests.py

+9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
DEFAULT_TIMEOUT = NETWORK_REQUEST_DEFAULT_TIMEOUT
2929

3030

31+
# requests can use one of these
32+
try:
33+
from simplejson.errors import ( # type: ignore # pylint: disable=unused-import
34+
JSONDecodeError,
35+
)
36+
except ModuleNotFoundError: # pragma: nocover
37+
from json.decoder import JSONDecodeError # noqa # pylint: disable=unused-import
38+
39+
3140
def add_default_timeout(fn: Callable, timeout: float) -> Callable:
3241
"""Add default timeout for requests methods."""
3342

Diff for: tests/test_cli/test_registry/test_push.py

+37-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"""Test module for Registry push methods."""
2020
import os
2121
from unittest import TestCase, mock
22-
from unittest.mock import mock_open
22+
from unittest.mock import mock_open, patch
2323

2424
import pytest
2525
from click import ClickException
@@ -37,6 +37,7 @@
3737

3838
@mock.patch("builtins.open", mock_open(read_data="opened_file"))
3939
@mock.patch("aea.cli.registry.push.check_is_author_logged_in")
40+
@mock.patch("aea.cli.registry.push.list_missing_packages", return_value=[])
4041
@mock.patch("aea.cli.registry.utils._rm_tarfiles")
4142
@mock.patch("aea.cli.registry.push.os.getcwd", return_value="cwd")
4243
@mock.patch("aea.cli.registry.push._compress_dir")
@@ -47,7 +48,7 @@
4748
"version": PublicIdMock.DEFAULT_VERSION,
4849
"author": "some_author",
4950
"name": "some_name",
50-
"protocols": ["protocol_id"],
51+
"protocols": ["some/protocol:0.1.2"],
5152
},
5253
)
5354
@mock.patch(
@@ -68,6 +69,7 @@ def test_push_item_positive(
6869
getcwd_mock,
6970
rm_tarfiles_mock,
7071
check_is_author_logged_in_mock,
72+
*_
7173
):
7274
"""Test for push_item positive result."""
7375
public_id = PublicIdMock(
@@ -83,12 +85,42 @@ def test_push_item_positive(
8385
"name": "some_name",
8486
"description": "some-description",
8587
"version": PublicIdMock.DEFAULT_VERSION,
86-
"protocols": ["protocol_id"],
88+
"protocols": ["some/protocol:0.1.2"],
8789
},
8890
is_auth=True,
8991
files={"file": open("file.1"), "readme": open("file.2")},
9092
)
9193

94+
@mock.patch("aea.cli.registry.push.os.path.exists", return_value=True)
95+
@mock.patch("aea.cli.registry.push.is_readme_present", return_value=True)
96+
def test_push_dependency_fail(
97+
self,
98+
is_readme_present_mock,
99+
path_exists_mock,
100+
request_api_mock,
101+
load_yaml_mock,
102+
compress_mock,
103+
getcwd_mock,
104+
rm_tarfiles_mock,
105+
check_is_author_logged_in_mock,
106+
*_
107+
):
108+
"""Test for push_item fails cause dependencies check."""
109+
public_id = PublicIdMock(
110+
name="some_name",
111+
author="some_author",
112+
version="{}".format(PublicIdMock.DEFAULT_VERSION),
113+
)
114+
115+
with patch(
116+
"aea.cli.registry.push.list_missing_packages",
117+
return_value=[("some", PublicId.from_str("some/pack:0.1.0"))],
118+
):
119+
with pytest.raises(
120+
ClickException, match="Found missed dependencies! Push canceled!"
121+
):
122+
push_item(ContextMock(), "some-type", public_id)
123+
92124
@mock.patch("aea.cli.registry.push.os.path.exists", return_value=True)
93125
@mock.patch("aea.cli.registry.push.is_readme_present", return_value=False)
94126
def test_push_item_positive_without_readme(
@@ -108,7 +140,7 @@ def test_push_item_positive_without_readme(
108140
"name": "some_name",
109141
"description": "some-description",
110142
"version": PublicIdMock.DEFAULT_VERSION,
111-
"protocols": ["protocol_id"],
143+
"protocols": ["some/protocol:0.1.2"],
112144
},
113145
is_auth=True,
114146
files={"file": open("opened_file", "r")},
@@ -124,6 +156,7 @@ def test_push_item_item_not_found(
124156
getcwd_mock,
125157
rm_tarfiles_mock,
126158
check_is_author_logged_in_mock,
159+
*_
127160
):
128161
"""Test for push_item - item not found."""
129162
with self.assertRaises(ClickException):

Diff for: tests/test_cli/test_registry/test_utils.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from json.decoder import JSONDecodeError
2424
from pathlib import Path
2525
from unittest import TestCase, mock
26-
from unittest.mock import MagicMock
26+
from unittest.mock import MagicMock, patch
2727

2828
import pytest
2929
from click import ClickException
@@ -41,6 +41,7 @@
4141
get_latest_version_available_in_registry,
4242
get_package_meta,
4343
is_auth_token_present,
44+
list_missing_packages,
4445
request_api,
4546
)
4647
from aea.cli.utils.exceptions import AEAConfigException
@@ -394,3 +395,22 @@ def test_get_latest_version_available_in_registry_remote_mode(*_mocks):
394395
context_mock, "protocol", DefaultMessage.protocol_id
395396
)
396397
assert result == DefaultMessage.protocol_id
398+
399+
400+
def test_list_missing_packages():
401+
"""Test 'list_missing_packages'."""
402+
packages = [("connection", PublicId.from_str("test/test:0.1.2"))]
403+
404+
resp_ok = MagicMock()
405+
resp_ok.status_code = 200
406+
with patch(
407+
"aea.cli.registry.utils._perform_registry_request", return_value=resp_ok
408+
):
409+
assert list_missing_packages(packages) == []
410+
411+
resp_404 = MagicMock()
412+
resp_404.status_code = 404
413+
with patch(
414+
"aea.cli.registry.utils._perform_registry_request", return_value=resp_404
415+
):
416+
assert list_missing_packages(packages) == packages

0 commit comments

Comments
 (0)