Skip to content

Commit 6364a6a

Browse files
authored
Merge pull request #762 from valory-xyz/release/1.58.0
Release `v1.58.0`
2 parents e45062b + f194be8 commit 6364a6a

File tree

31 files changed

+242
-68
lines changed

31 files changed

+242
-68
lines changed

.spelling

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# global dictionary is at the start, file overrides afterwards
44
# one word per line, to define a file override use ' - filename'
55
# where filename is relative to this configuration file
6+
fraxtal
67
Flashbots
78
liveness
89
pre-shared

HISTORY.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Release History - open AEA
22

3+
## 1.58.0 (2024-10-03)
4+
5+
AEA:
6+
- Adds support for dictionary overrides #750, #761
7+
38
## 1.57.0 (2024-09-24)
49

510
AEA:

SECURITY.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ The following table shows which versions of `open-aea` are currently being suppo
88

99
| Version | Supported |
1010
|------------| ------------------ |
11-
| `1.57.x` | :white_check_mark: |
12-
| `< 1.57.0` | :x: |
11+
| `1.58.x` | :white_check_mark: |
12+
| `< 1.58.0` | :x: |
1313

1414
## Reporting a Vulnerability
1515

aea/__version__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
__title__ = "open-aea"
2424
__description__ = "Open Autonomous Economic Agent framework (without vendor lock-in)"
2525
__url__ = "https://github.com/valory-xyz/open-aea.git"
26-
__version__ = "1.57.0"
26+
__version__ = "1.58.0"
2727
__author__ = "Valory AG"
2828
__license__ = "Apache-2.0"
2929
__copyright__ = "2021 Valory AG, 2019 Fetch.AI Limited"

aea/configurations/validation.py

+25-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
# ------------------------------------------------------------------------------
33
#
4-
# Copyright 2022-2023 Valory AG
4+
# Copyright 2022-2024 Valory AG
55
# Copyright 2018-2021 Fetch.AI Limited
66
#
77
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,11 +18,13 @@
1818
#
1919
# ------------------------------------------------------------------------------
2020
"""Implementation of the configuration validation."""
21+
2122
import inspect
2223
import json
2324
import os
2425
from collections import OrderedDict
2526
from copy import deepcopy
27+
from functools import reduce
2628
from pathlib import Path
2729
from typing import Any, Dict, Iterator, List, Optional, Tuple
2830

@@ -36,7 +38,7 @@
3638
from aea.configurations.constants import AGENT
3739
from aea.configurations.data_types import ComponentId, ComponentType, PublicId
3840
from aea.exceptions import AEAValidationError
39-
from aea.helpers.base import dict_to_path_value
41+
from aea.helpers.base import dict_to_path_value, update_nested_dict
4042
from aea.helpers.env_vars import is_env_variable
4143
from aea.helpers.io import open_file
4244

@@ -295,12 +297,26 @@ def validate_data_with_pattern(
295297
excludes_: List[Tuple[str]] = []
296298
else:
297299
excludes_ = excludes
298-
pattern_path_value = {
300+
original_config = {
299301
tuple(path): value for path, value in dict_to_path_value(pattern)
300302
}
301-
data_path_value = {tuple(path): value for path, value in dict_to_path_value(data)}
303+
overrides = {tuple(path): value for path, value in dict_to_path_value(data)}
302304
errors = []
303305

306+
# this is a workaround to fix the type of numeric keys as they can only be represented as strs in the json overrides
307+
for path in original_config:
308+
path_as_str = tuple(map(str, path))
309+
if path_as_str in overrides and path not in overrides:
310+
value = overrides[path_as_str]
311+
del overrides[path_as_str]
312+
up_to_last_key = data
313+
for key in path_as_str[:-1]:
314+
up_to_last_key = up_to_last_key[key]
315+
del up_to_last_key[path_as_str[-1]]
316+
overrides[path] = value
317+
vals = reduce(lambda d, key: {key: d}, reversed(path), value)
318+
update_nested_dict(data, vals)
319+
304320
def check_excludes(path: Tuple[str, ...]) -> bool:
305321
for exclude in excludes_:
306322
if len(exclude) > len(path): # pragma: nocover
@@ -315,25 +331,25 @@ def is_a_dict_override(path: Tuple[str, ...]) -> bool:
315331
flag = False
316332
while len(path) > 0:
317333
path = path[:-1]
318-
if path in pattern_path_value:
319-
pattern_value = pattern_path_value[path]
334+
if path in original_config:
335+
pattern_value = original_config[path]
320336
flag = isinstance(pattern_value, OrderedDict)
321337
break
322338
return flag
323339

324-
for path, new_value in data_path_value.items():
340+
for path, new_value in overrides.items():
325341
if check_excludes(path):
326342
continue
327343

328-
if path not in pattern_path_value:
344+
if path not in original_config:
329345
if not is_a_dict_override(path=(*path,)):
330346
errors.append(
331347
f"Attribute `{'.'.join(path)}` is not allowed to be updated!"
332348
)
333349

334350
continue
335351

336-
pattern_value = pattern_path_value[path]
352+
pattern_value = original_config[path]
337353

338354
if pattern_value is None:
339355
# not possible to determine data type for optional value not set

aea/helpers/base.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
# ------------------------------------------------------------------------------
33
#
4-
# Copyright 2022-2023 Valory AG
4+
# Copyright 2022-2024 Valory AG
55
# Copyright 2018-2021 Fetch.AI Limited
66
#
77
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -711,10 +711,14 @@ def dict_to_path_value(
711711
"""Convert dict to sequence of terminal path build of keys and value."""
712712
path = path or []
713713
for key, value in data.items():
714+
# terminal value
714715
if isinstance(value, Mapping) and value:
715-
# terminal value
716+
# yielding here allows for higher level dict overriding
717+
yield path + [key], value
718+
# recursing to the next level of the dict
716719
for p, v in dict_to_path_value(value, path + [key]):
717720
yield p, v
721+
# non-terminal value
718722
else:
719723
yield path + [key], value
720724

@@ -1087,3 +1091,13 @@ def prepend_if_not_absolute(path: PathLike, prefix: PathLike) -> PathLike:
10871091
:return: the same path if absolute, else the prepended path.
10881092
"""
10891093
return path if Path(path).is_absolute() else Path(prefix) / path
1094+
1095+
1096+
def update_nested_dict(dict_: dict, nested_update: dict) -> dict:
1097+
"""Update a nested dictionary."""
1098+
for key, value in nested_update.items():
1099+
if isinstance(value, dict):
1100+
dict_[key] = update_nested_dict(dict_.get(key, {}), value)
1101+
else:
1102+
dict_[key] = value
1103+
return dict_

aea/helpers/env_vars.py

+91-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
# ------------------------------------------------------------------------------
33
#
4-
# Copyright 2022-2023 Valory AG
4+
# Copyright 2022-2024 Valory AG
55
# Copyright 2018-2019 Fetch.AI Limited
66
#
77
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,17 +34,47 @@
3434

3535

3636
ENV_VARIABLE_RE = re.compile(r"^\$\{(([A-Z0-9_]+):?)?([a-z]+)?(:(.+))?}$")
37+
MODELS = "models"
38+
ARGS = "args"
39+
ARGS_LEVEL_FROM_MODELS = 2
40+
ARG_LEVEL_FROM_MODELS = ARGS_LEVEL_FROM_MODELS + 1
41+
RESTRICTION_EXCEPTIONS = frozenset({"setup", "genesis_config"})
3742

3843

3944
def is_env_variable(value: Any) -> bool:
4045
"""Check is variable string with env variable pattern."""
4146
return isinstance(value, str) and bool(ENV_VARIABLE_RE.match(value))
4247

4348

44-
def export_path_to_env_var_string(export_path: List[str]) -> str:
45-
"""Conver export path to environment variable string."""
49+
def restrict_model_args(export_path: List[str]) -> Tuple[List[str], List[str]]:
50+
"""Do not allow more levels than one for a model's argument."""
51+
restricted = []
52+
result = []
53+
for i, current_path in enumerate(export_path):
54+
result.append(current_path)
55+
args_level = i + ARGS_LEVEL_FROM_MODELS
56+
arg_level = i + ARG_LEVEL_FROM_MODELS
57+
if (
58+
current_path == MODELS
59+
and arg_level < len(export_path)
60+
and export_path[args_level] == ARGS
61+
and export_path[arg_level] not in RESTRICTION_EXCEPTIONS
62+
):
63+
# do not allow more levels than one for a model's argument
64+
arg_content_level = arg_level + 1
65+
result.extend(export_path[i + 1 : arg_content_level])
66+
# store the restricted part of the path
67+
for j in range(arg_content_level, len(export_path)):
68+
restricted.append(export_path[j])
69+
break
70+
return restricted, result
71+
72+
73+
def export_path_to_env_var_string(export_path: List[str]) -> Tuple[List[str], str]:
74+
"""Convert export path to environment variable string."""
75+
restricted, export_path = restrict_model_args(export_path)
4676
env_var_string = "_".join(map(str, export_path))
47-
return env_var_string.upper()
77+
return restricted, env_var_string.upper()
4878

4979

5080
NotSet = object()
@@ -149,7 +179,7 @@ def apply_env_variables(
149179
data,
150180
env_variables,
151181
default_value,
152-
default_var_name=export_path_to_env_var_string(export_path=path),
182+
default_var_name=export_path_to_env_var_string(export_path=path)[1],
153183
)
154184

155185
return data
@@ -242,35 +272,76 @@ def is_strict_list(data: Union[List, Tuple]) -> bool:
242272
return is_strict
243273

244274

275+
def list_to_nested_dict(lst: list, val: Any) -> dict:
276+
"""Convert a list to a nested dict."""
277+
nested_dict = val
278+
for item in reversed(lst):
279+
nested_dict = {item: nested_dict}
280+
return nested_dict
281+
282+
283+
def ensure_dict(dict_: Dict[str, Union[dict, str]]) -> dict:
284+
"""Return the given dictionary converting any values which are json strings as dicts."""
285+
return {k: json.loads(v) for k, v in dict_.items() if isinstance(v, str)}
286+
287+
288+
def ensure_json_content(dict_: dict) -> dict:
289+
"""Return the given dictionary converting any nested dictionary values as json strings."""
290+
return {k: json.dumps(v) for k, v in dict_.items() if isinstance(v, dict)}
291+
292+
293+
def merge_dicts(a: dict, b: dict) -> dict:
294+
"""Merge two dictionaries."""
295+
# shallow copy of `a`
296+
merged = {**a}
297+
for key, value in b.items():
298+
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
299+
# recursively merge nested dictionaries
300+
merged[key] = merge_dicts(merged[key], value)
301+
else:
302+
# if not a nested dictionary, just take the value from `b`
303+
merged[key] = value
304+
return merged
305+
306+
245307
def generate_env_vars_recursively(
246308
data: Union[Dict, List],
247309
export_path: List[str],
248310
) -> Dict:
249311
"""Generate environment variables recursively."""
250-
env_var_dict = {}
312+
env_var_dict: Dict[str, Any] = {}
251313

252314
if isinstance(data, dict):
253315
for key, value in data.items():
254-
env_var_dict.update(
255-
generate_env_vars_recursively(
256-
data=value,
257-
export_path=[*export_path, key],
258-
)
316+
res = generate_env_vars_recursively(
317+
data=value,
318+
export_path=[*export_path, key],
259319
)
320+
if res:
321+
env_var = list(res.keys())[0]
322+
if env_var in env_var_dict:
323+
dicts = (ensure_dict(dict_) for dict_ in (env_var_dict, res))
324+
res = ensure_json_content(merge_dicts(*dicts))
325+
env_var_dict.update(res)
260326
elif isinstance(data, list):
261327
if is_strict_list(data=data):
262-
env_var_dict[
263-
export_path_to_env_var_string(export_path=export_path)
264-
] = json.dumps(data, separators=(",", ":"))
328+
restricted, path = export_path_to_env_var_string(export_path=export_path)
329+
if restricted:
330+
env_var_dict[path] = json.dumps(list_to_nested_dict(restricted, data))
331+
else:
332+
env_var_dict[path] = json.dumps(data, separators=(",", ":"))
265333
else:
266334
for key, value in enumerate(data):
267-
env_var_dict.update(
268-
generate_env_vars_recursively(
269-
data=value,
270-
export_path=[*export_path, key],
271-
)
335+
res = generate_env_vars_recursively(
336+
data=value,
337+
export_path=[*export_path, key],
272338
)
339+
env_var_dict.update(res)
273340
else:
274-
env_var_dict[export_path_to_env_var_string(export_path=export_path)] = data
341+
restricted, path = export_path_to_env_var_string(export_path=export_path)
342+
if restricted:
343+
env_var_dict[path] = json.dumps(list_to_nested_dict(restricted, data))
344+
else:
345+
env_var_dict[path] = data
275346

276347
return env_var_dict

deploy-image/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN apk add --no-cache go
1616

1717
# aea installation
1818
RUN pip install --upgrade pip
19-
RUN pip install --upgrade --force-reinstall open-aea[all]==1.57.0 "open-aea-cli-ipfs<2.0.0,>=1.57.0"
19+
RUN pip install --upgrade --force-reinstall open-aea[all]==1.58.0 "open-aea-cli-ipfs<2.0.0,>=1.58.0"
2020

2121
# directories and aea cli config
2222
WORKDIR /home/agents

deploy-image/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The example uses the `fetchai/my_first_aea` project. You will likely want to mod
1111
Install subversion, then download the example directory to your local working directory
1212

1313
``` bash
14-
svn checkout https://github.com/valory-xyz/open-aea/tags/v1.57.0/packages packages
14+
svn checkout https://github.com/valory-xyz/open-aea/tags/v1.58.0/packages packages
1515
```
1616

1717
### Modify scripts

develop-image/docker-env.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22

33
# Swap the following lines if you want to work with 'latest'
4-
DOCKER_IMAGE_TAG=valory/open-aea-develop:1.57.0
4+
DOCKER_IMAGE_TAG=valory/open-aea-develop:1.58.0
55
# DOCKER_IMAGE_TAG=valory/open-aea-develop:latest
66

77
DOCKER_BUILD_CONTEXT_DIR=..

docs/api/helpers/base.md

+10
Original file line numberDiff line numberDiff line change
@@ -838,3 +838,13 @@ Prepend a path with a prefix, but only if not absolute
838838

839839
the same path if absolute, else the prepended path.
840840

841+
<a id="aea.helpers.base.update_nested_dict"></a>
842+
843+
#### update`_`nested`_`dict
844+
845+
```python
846+
def update_nested_dict(dict_: dict, nested_update: dict) -> dict
847+
```
848+
849+
Update a nested dictionary.
850+

0 commit comments

Comments
 (0)