Skip to content

Commit b1f455d

Browse files
authored
Filter endpoint tests, unittest asserts, pylint cleanups (Closes #45) (#60)
* Add filter endpoint tests, switch to unittest provided asserts, make pylint happier * Properly split /api/filters endpoint handlers
1 parent b050663 commit b1f455d

20 files changed

+1238
-867
lines changed

.pylintrc

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
extension-pkg-whitelist=
77

88
# Specify a score threshold to be exceeded before program exits with error.
9-
fail-under=10.0
9+
fail-under=10
1010

1111
# Add files or directories to the blacklist. They should be base names, not
1212
# paths.
@@ -589,6 +589,3 @@ valid-metaclass-classmethod-first-arg=cls
589589
# "BaseException, Exception".
590590
overgeneral-exceptions=BaseException,
591591
Exception
592-
593-
[MESSAGES CONTROL]
594-
disable = C0330, C0326

gramps_webapi/api/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .resources.citation import CitationResource, CitationsResource
1515
from .resources.event import EventResource, EventsResource
1616
from .resources.family import FamiliesResource, FamilyResource
17-
from .resources.filters import FilterResource
17+
from .resources.filters import FilterResource, FiltersResource
1818
from .resources.media import MediaObjectResource, MediaObjectsResource
1919
from .resources.metadata import MetadataResource
2020
from .resources.name_groups import NameGroupsResource
@@ -80,7 +80,8 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
8080
register_endpt(BookmarkResource, "/bookmarks/<string:namespace>", "bookmark")
8181
register_endpt(BookmarksResource, "/bookmarks/", "bookmarks")
8282
# Filter
83-
register_endpt(FilterResource, "/filters/<string:namespace>", "filter")
83+
register_endpt(FilterResource, "/filters/<string:namespace>/<string:name>", "filter")
84+
register_endpt(FiltersResource, "/filters/<string:namespace>", "filters")
8485
# Translate
8586
register_endpt(TranslationResource, "/translations/<string:isocode>", "translation")
8687
register_endpt(TranslationsResource, "/translations/", "translations")

gramps_webapi/api/resources/filters.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"""Gramps filter interface."""
22

33
import json
4-
from typing import Dict, List, Set
4+
from typing import Any, Dict, List, Set
55

66
import gramps.gen.filters as filters
77
from flask import Response, abort
88
from gramps.gen.db.base import DbReadBase
99
from gramps.gen.filters import GenericFilter
10-
from marshmallow import Schema, ValidationError, fields
11-
from webargs import validate
10+
from marshmallow import Schema
11+
from webargs import ValidationError, fields, validate
1212
from webargs.flaskparser import use_args
1313

1414
from ...const import GRAMPS_NAMESPACES
@@ -29,13 +29,14 @@
2929
}
3030

3131

32-
def get_filter_rules(args: Dict[str, str], namespace: str) -> List[Dict]:
32+
def get_filter_rules(args: Dict[str, Any], namespace: str) -> List[Dict]:
3333
"""Return a list of available filter rules for a namespace."""
3434
rule_list = []
3535
for rule_class in _RULES_LOOKUP[namespace]:
3636
add_rule = True
37-
if args.get("rule") and args["rule"] != rule_class.__name__:
38-
add_rule = False
37+
if "rules" in args and args["rules"]:
38+
if rule_class.__name__ not in args["rules"]:
39+
add_rule = False
3940
if add_rule:
4041
rule_list.append(
4142
{
@@ -46,17 +47,20 @@ def get_filter_rules(args: Dict[str, str], namespace: str) -> List[Dict]:
4647
"rule": rule_class.__name__,
4748
}
4849
)
50+
if "rules" in args and len(args["rules"]) != len(rule_list):
51+
abort(404)
4952
return rule_list
5053

5154

52-
def get_custom_filters(args: Dict[str, str], namespace: str) -> List[Dict]:
55+
def get_custom_filters(args: Dict[str, Any], namespace: str) -> List[Dict]:
5356
"""Return a list of custom filters for a namespace."""
5457
filter_list = []
5558
filters.reload_custom_filters()
5659
for filter_class in filters.CustomFilters.get_filters(namespace):
5760
add_filter = True
58-
if args.get("filter") and args["filter"] != filter_class.get_name():
59-
add_filter = False
61+
if "filters" in args and args["filters"]:
62+
if filter_class.get_name() not in args["filters"]:
63+
add_filter = False
6064
if add_filter:
6165
filter_list.append(
6266
{
@@ -74,6 +78,8 @@ def get_custom_filters(args: Dict[str, str], namespace: str) -> List[Dict]:
7478
],
7579
}
7680
)
81+
if "filters" in args and len(args["filters"]) != len(filter_list):
82+
abort(404)
7783
return filter_list
7884

7985

@@ -113,7 +119,7 @@ def apply_filter(db_handle: DbReadBase, args: Dict, namespace: str) -> List[Hand
113119
for filter_class in filters.CustomFilters.get_filters(namespace):
114120
if args["filter"] == filter_class.get_name():
115121
return filter_class.apply(db_handle)
116-
abort(400)
122+
abort(404)
117123

118124
try:
119125
filter_parms = FilterSchema().load(json.loads(args["rules"]))
@@ -143,21 +149,31 @@ class FilterSchema(Schema):
143149
validate=validate.OneOf(["and", "or", "xor", "one"]),
144150
)
145151
invert = fields.Boolean(required=False, missing=False)
146-
rules = fields.List(fields.Nested(RuleSchema))
152+
rules = fields.List(
153+
fields.Nested(RuleSchema), required=True, validate=validate.Length(min=1)
154+
)
147155

148156

149157
class CustomFilterSchema(FilterSchema):
150158
"""Structure for a custom filter."""
151159

152160
name = fields.Str(required=True, validate=validate.Length(min=1))
153161
comment = fields.Str(required=False)
162+
rules = fields.List(
163+
fields.Nested(RuleSchema), required=True, validate=validate.Length(min=1)
164+
)
154165

155166

156-
class FilterResource(ProtectedResource, GrampsJSONEncoder):
157-
"""Filter resource."""
167+
class FiltersResource(ProtectedResource, GrampsJSONEncoder):
168+
"""Filters resource."""
158169

159170
@use_args(
160-
{"filter": fields.Str(), "rule": fields.Str()},
171+
{
172+
"filters": fields.DelimitedList(
173+
fields.Str(validate=validate.Length(min=1))
174+
),
175+
"rules": fields.DelimitedList(fields.Str(validate=validate.Length(min=1))),
176+
},
161177
location="query",
162178
)
163179
def get(self, args: Dict[str, str], namespace: str) -> Response:
@@ -168,10 +184,10 @@ def get(self, args: Dict[str, str], namespace: str) -> Response:
168184
abort(404)
169185

170186
rule_list = get_filter_rules(args, namespace)
171-
if args.get("rule") and not args.get("filter"):
187+
if "rules" in args and "filters" not in args:
172188
return self.response(200, {"rules": rule_list})
173189
filter_list = get_custom_filters(args, namespace)
174-
if args.get("filter") and not args.get("rule"):
190+
if "filters" in args and "rules" not in args:
175191
return self.response(200, {"filters": filter_list})
176192
return self.response(200, {"filters": filter_list, "rules": rule_list})
177193

@@ -212,14 +228,30 @@ def put(self, args: Dict, namespace: str) -> Response:
212228
)
213229
return abort(404)
214230

231+
232+
class FilterResource(ProtectedResource, GrampsJSONEncoder):
233+
"""Filter resource."""
234+
235+
def get(self, namespace: str, name: str) -> Response:
236+
"""Get a custom filter."""
237+
try:
238+
namespace = GRAMPS_NAMESPACES[namespace]
239+
except KeyError:
240+
abort(404)
241+
242+
args = {"filters": [name]}
243+
filter_list = get_custom_filters(args, namespace)
244+
if len(filter_list) == 0:
245+
abort(404)
246+
return self.response(200, filter_list[0])
247+
215248
@use_args(
216249
{
217-
"name": fields.Str(required=True),
218-
"force": fields.Boolean(required=False, missing=False),
250+
"force": fields.Str(validate=validate.Length(equal=0)),
219251
},
220-
location="json",
252+
location="query",
221253
)
222-
def delete(self, args: Dict, namespace: str) -> Response:
254+
def delete(self, args: Dict, namespace: str, name: str) -> Response:
223255
"""Delete a custom filter."""
224256
try:
225257
namespace = GRAMPS_NAMESPACES[namespace]
@@ -229,17 +261,15 @@ def delete(self, args: Dict, namespace: str) -> Response:
229261
filters.reload_custom_filters()
230262
custom_filters = filters.CustomFilters.get_filters(namespace)
231263
for custom_filter in custom_filters:
232-
if args["name"] == custom_filter.get_name():
264+
if name == custom_filter.get_name():
233265
filter_set = set()
234266
self._find_dependent_filters(namespace, custom_filter, filter_set)
235267
if len(filter_set) > 1:
236268
if "force" not in args or not args["force"]:
237269
abort(405)
238270
list(map(custom_filters.remove, filter_set))
239271
filters.CustomFilters.save()
240-
return self.response(
241-
200, {"message": "Deleted filter: " + args["name"]}
242-
)
272+
return self.response(200, {"message": "Deleted filter: " + name})
243273
return abort(404)
244274

245275
def _find_dependent_filters(

gramps_webapi/data/apispec.yaml

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,16 +1825,16 @@ paths:
18251825
required: true
18261826
type: string
18271827
description: "The namespace or category for the filters."
1828-
- name: rule
1828+
- name: rules
18291829
in: query
18301830
required: false
18311831
type: string
1832-
description: "The name of a specific filter rule to be returned."
1833-
- name: filter
1832+
description: "A comma delimited list of specific filter rules to be returned."
1833+
- name: filters
18341834
in: query
18351835
required: false
18361836
type: string
1837-
description: "The name of a specific custom filter to be returned."
1837+
description: "A comma delimited list of specific custom filters to be returned."
18381838
responses:
18391839
200:
18401840
description: "OK: Successful operation."
@@ -1908,10 +1908,39 @@ paths:
19081908
403:
19091909
description: "Forbidden: Bad credentials, authentication failed."
19101910

1911+
/filters/{namespace}/{name}:
1912+
get:
1913+
tags:
1914+
- filters
1915+
summary: "Get a custom filter for a given namespace or category."
1916+
operationId: getFilter
1917+
security:
1918+
- Bearer: []
1919+
parameters:
1920+
- name: namespace
1921+
in: path
1922+
required: true
1923+
type: string
1924+
description: "The namespace or category for the custom filter."
1925+
- name: name
1926+
in: path
1927+
required: true
1928+
type: string
1929+
description: "The name of a custom filter."
1930+
responses:
1931+
200:
1932+
description: "OK: Successful operation."
1933+
schema:
1934+
$ref: "#/definitions/CustomFilter"
1935+
401:
1936+
description: "Unauthorized: Missing authorization header."
1937+
404:
1938+
description: "Not Found: Namespace or filter not found."
1939+
19111940
delete:
19121941
tags:
19131942
- filters
1914-
summary: "Delete a custom filter."
1943+
summary: "Delete a custom filter in a given namespace or category."
19151944
operationId: deleteFilter
19161945
security:
19171946
- Bearer: []
@@ -1921,12 +1950,16 @@ paths:
19211950
required: true
19221951
type: string
19231952
description: "The namespace or category for the custom filter."
1924-
- name: filter
1925-
in: body
1953+
- name: name
1954+
in: path
19261955
required: true
1927-
description: "The custom filter to delete."
1928-
schema:
1929-
$ref: "#/definitions/CustomFilterObject"
1956+
type: string
1957+
description: "The name of the custom filter."
1958+
- name: force
1959+
in: query
1960+
required: false
1961+
type: string
1962+
description: "Force delete custom filter and all filters that depend upon it."
19301963
responses:
19311964
200:
19321965
description: "OK: Successful operation."
@@ -4045,24 +4078,6 @@ definitions:
40454078
items:
40464079
$ref: "#/definitions/FilterRule"
40474080

4048-
##############################################################################
4049-
# Model - CustomFilterObject
4050-
##############################################################################
4051-
4052-
CustomFilterObject:
4053-
type: object
4054-
required:
4055-
- name
4056-
properties:
4057-
name:
4058-
type: string
4059-
description: "Name of the custom rule to delete."
4060-
example: "MyTestRule"
4061-
force:
4062-
type: boolean
4063-
description: "Indicator to approve deletion of all dependent filters if any exist."
4064-
example: true
4065-
40664081
##############################################################################
40674082
# Model - Translations
40684083
##############################################################################

tests/test_endpoints/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
import gramps_webapi.app
1414
from gramps_webapi.app import create_app
1515
from gramps_webapi.const import ENV_CONFIG_FILE, TEST_EXAMPLE_GRAMPS_CONFIG
16-
17-
from .. import TEST_GRAMPSHOME, ExampleDbSQLite
16+
from tests import TEST_GRAMPSHOME, ExampleDbSQLite
1817

1918

2019
def get_object_count(gramps_object):

0 commit comments

Comments
 (0)