Skip to content

Commit 7018f45

Browse files
fix: Use +and for X-Filter header and support filtering on multiple list entries (#476)
## 📝 Description This change adds support for filtering on multiple entries of the same response list when calling list endpoints. This works by flattening out any parsed list arguments and appending all values to a `+and` filter. For example: ```bash linode-cli linodes ls --tags foo --tags bar ``` This command would previously ignore the first specified tag and only filter on the second tag. With this fix, response rows will be required to have both specified tags. ## ✔️ How to Test ```bash make testunit ```
1 parent c25d37f commit 7018f45

File tree

4 files changed

+62
-12
lines changed

4 files changed

+62
-12
lines changed

linodecli/api_request.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,26 @@ def _build_filter_header(
7777
if filter_header is not None:
7878
return json.dumps(filter_header)
7979

80-
filters = vars(parsed_args)
80+
parsed_args_dict = vars(parsed_args)
8181

8282
# remove URL parameters
8383
for p in operation.params:
84-
if p.name in filters:
85-
del filters[p.name]
84+
if p.name in parsed_args_dict:
85+
del parsed_args_dict[p.name]
8686

87-
# remove empty filters
88-
filters = {k: v for k, v in filters.items() if v is not None}
87+
# The "+and" list to be used in the filter header
88+
filter_list = []
8989

90-
if filters:
91-
return json.dumps(filters)
90+
for k, v in parsed_args_dict.items():
91+
if v is None:
92+
continue
93+
94+
# If this is a list, flatten it out
95+
new_filters = [{k: j} for j in v] if isinstance(v, list) else [{k: v}]
96+
filter_list.extend(new_filters)
97+
98+
if len(filter_list) > 0:
99+
return json.dumps({"+and": filter_list})
92100

93101
return None
94102

linodecli/operation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ def parse_args(
272272
"--" + attr.name,
273273
type=TYPES[attr.item_type],
274274
metavar=attr.name,
275+
action="append",
275276
nargs="?",
276277
)
277278
else:

tests/unit/conftest.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,18 @@ def list_operation():
118118
"foo/bar",
119119
"get info",
120120
[],
121-
ResponseModel([ModelAttr("filterable_result", True, True, "string")]),
121+
ResponseModel(
122+
[
123+
ModelAttr("filterable_result", True, True, "string"),
124+
ModelAttr(
125+
"filterable_list_result",
126+
True,
127+
True,
128+
"array",
129+
item_type="string",
130+
),
131+
]
132+
),
122133
[],
123134
)
124135

tests/unit/test_api_request.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,39 @@ def test_build_request_url_post(self, mock_cli, create_operation):
8888

8989
def test_build_filter_header(self, list_operation):
9090
result = api_request._build_filter_header(
91-
list_operation, SimpleNamespace(filterable_result="bar")
91+
list_operation,
92+
SimpleNamespace(
93+
filterable_result="bar",
94+
filterable_list_result=["foo", "bar"],
95+
),
9296
)
9397

94-
assert json.dumps({"filterable_result": "bar"}) == result
98+
assert (
99+
json.dumps(
100+
{
101+
"+and": [
102+
{"filterable_result": "bar"},
103+
{"filterable_list_result": "foo"},
104+
{"filterable_list_result": "bar"},
105+
]
106+
}
107+
)
108+
== result
109+
)
95110

96111
def test_do_request_get(self, mock_cli, list_operation):
97112
mock_response = Mock(status_code=200, reason="OK")
98113

99114
def validate_http_request(url, headers=None, data=None, **kwargs):
100115
assert url == "http://localhost/foo/bar?page=1&page_size=100"
101116
assert headers["X-Filter"] == json.dumps(
102-
{"filterable_result": "cool"}
117+
{
118+
"+and": [
119+
{"filterable_result": "cool"},
120+
{"filterable_list_result": "foo"},
121+
{"filterable_list_result": "bar"},
122+
]
123+
}
103124
)
104125
assert "Authorization" in headers
105126
assert data is None
@@ -108,7 +129,16 @@ def validate_http_request(url, headers=None, data=None, **kwargs):
108129

109130
with patch("linodecli.api_request.requests.get", validate_http_request):
110131
result = api_request.do_request(
111-
mock_cli, list_operation, ["--filterable_result", "cool"]
132+
mock_cli,
133+
list_operation,
134+
[
135+
"--filterable_result",
136+
"cool",
137+
"--filterable_list_result",
138+
"foo",
139+
"--filterable_list_result",
140+
"bar",
141+
],
112142
)
113143

114144
assert result == mock_response

0 commit comments

Comments
 (0)