Skip to content

Commit 1b23880

Browse files
authored
Add DNA match parser resource (#604)
* Add DNA match parser resource * Fix typing and linting issues * Update apispec, version -> 2.8.0 * Add unit test
1 parent 68b7e34 commit 1b23880

File tree

7 files changed

+104
-44
lines changed

7 files changed

+104
-44
lines changed

.pre-commit-config.yaml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
repos:
2-
- repo: https://github.com/pre-commit/mirrors-isort
3-
rev: v5.10.1
2+
- repo: https://github.com/pycqa/isort
3+
rev: 5.13.2
44
hooks:
55
- id: isort
66
args: ["--profile", "black"]
77
- repo: https://github.com/psf/black
8-
rev: 24.8.0
8+
rev: 24.10.0
99
hooks:
1010
- id: black
1111
- repo: https://github.com/pre-commit/mirrors-mypy
12-
rev: v1.11.2
12+
rev: v1.14.1
1313
hooks:
1414
- id: mypy
15-
args: [--ignore-missing-imports]
15+
args: [--ignore-missing-imports, --no-strict-optional]
16+
additional_dependencies:
17+
- types-setuptools
1618
- repo: https://github.com/PyCQA/pylint
17-
rev: v3.2.7
19+
rev: v3.3.3
1820
hooks:
1921
- id: pylint
2022
stages: [commit]
2123
args:
2224
- --score=n
25+
- --disable=import-error,arguments-differ,too-many-locals
2326
- repo: https://github.com/PyCQA/pydocstyle
2427
rev: 6.3.0
2528
hooks:

gramps_webapi/_version.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@
1818
#
1919

2020
# make sure to match this version with the one in apispec.yaml
21-
__version__ = "2.7.0"
21+
22+
"""Version information."""
23+
24+
__version__ = "2.8.0"

gramps_webapi/api/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from .resources.chat import ChatResource
3838
from .resources.citations import CitationResource, CitationsResource
3939
from .resources.config import ConfigResource, ConfigsResource
40-
from .resources.dna import PersonDnaMatchesResource
40+
from .resources.dna import PersonDnaMatchesResource, DnaMatchParserResource
4141
from .resources.events import EventResource, EventSpanResource, EventsResource
4242
from .resources.export_media import MediaArchiveFileResource, MediaArchiveResource
4343
from .resources.exporters import (
@@ -235,6 +235,8 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
235235
# Translations
236236
register_endpt(TranslationResource, "/translations/<string:language>", "translation")
237237
register_endpt(TranslationsResource, "/translations/", "translations")
238+
# Parsers
239+
register_endpt(DnaMatchParserResource, "/parsers/dna-match", "dna-match-parser")
238240
# Relations
239241
register_endpt(
240242
RelationResource,

gramps_webapi/api/resources/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def _parse_object(self) -> GrampsObject:
161161
try:
162162
obj_dict = fix_object_dict(obj_dict)
163163
except ValueError as exc:
164-
abort_with_message(400, "Error while processing object")
164+
abort_with_message(400, f"Error while processing object: {exc}")
165165
if not validate_object_dict(obj_dict):
166166
abort_with_message(400, "Schema validation failed")
167167
return from_json(json.dumps(obj_dict))

gramps_webapi/api/resources/dna.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,32 @@
2121

2222
"""DNA resources."""
2323

24-
from typing import Any, Dict, List, Optional, Union
24+
from __future__ import annotations
25+
26+
from typing import Any, Union
2527

2628
from flask import abort
2729
from gramps.gen.const import GRAMPS_LOCALE as glocale
2830
from gramps.gen.db.base import DbReadBase
2931
from gramps.gen.errors import HandleError
30-
from gramps.gen.lib import Person, PersonRef
32+
from gramps.gen.lib import Note, Person, PersonRef
3133
from gramps.gen.relationship import get_relationship_calculator
3234
from gramps.gen.utils.grampslocale import GrampsLocale
3335
from webargs import fields, validate
3436

3537
from gramps_webapi.api.people_families_cache import CachePeopleFamiliesProxy
38+
from gramps_webapi.types import ResponseReturnValue
3639

3740
from ...types import Handle
3841
from ..util import get_db_handle, get_locale_for_language, use_args
39-
from .util import get_person_profile_for_handle
4042
from . import ProtectedResource
43+
from .util import get_person_profile_for_handle
4144

4245
SIDE_UNKNOWN = "U"
4346
SIDE_MATERNAL = "M"
4447
SIDE_PATERNAL = "P"
4548

46-
Segment = Dict[str, Union[float, int, str]]
49+
Segment = dict[str, Union[float, int, str]]
4750

4851

4952
class PersonDnaMatchesResource(ProtectedResource):
@@ -57,7 +60,7 @@ class PersonDnaMatchesResource(ProtectedResource):
5760
},
5861
location="query",
5962
)
60-
def get(self, args: Dict, handle: str):
63+
def get(self, args: dict, handle: str):
6164
"""Get the DNA match data."""
6265
db_handle = CachePeopleFamiliesProxy(get_db_handle())
6366

@@ -84,12 +87,24 @@ def get(self, args: Dict, handle: str):
8487
return matches
8588

8689

90+
class DnaMatchParserResource(ProtectedResource):
91+
"""DNA match parser resource."""
92+
93+
@use_args(
94+
{"string": fields.Str(required=True)},
95+
location="json",
96+
)
97+
def post(self, args: dict) -> ResponseReturnValue:
98+
"""Parse DNA match string."""
99+
return parse_raw_dna_match_string(args["string"])
100+
101+
87102
def get_match_data(
88103
db_handle: DbReadBase,
89104
person: Person,
90105
association: PersonRef,
91106
locale: GrampsLocale = glocale,
92-
) -> Dict[str, Any]:
107+
) -> dict[str, Any]:
93108
"""Get the DNA match data in the appropriate format."""
94109
relationship = get_relationship_calculator(reinit=True, clocale=locale)
95110
associate = db_handle.get_person_from_handle(association.ref)
@@ -154,19 +169,27 @@ def get_match_data(
154169

155170

156171
def get_segments_from_note(
157-
db_handle: DbReadBase, handle: Handle, side: Optional[str] = None
158-
) -> List[Segment]:
172+
db_handle: DbReadBase, handle: Handle, side: str | None = None
173+
) -> list[Segment]:
159174
"""Get the segements from a note handle."""
160-
note = db_handle.get_note_from_handle(handle)
175+
note: Note = db_handle.get_note_from_handle(handle)
176+
raw_string: str = note.get()
177+
return parse_raw_dna_match_string(raw_string, side=side)
178+
179+
180+
def parse_raw_dna_match_string(
181+
raw_string: str, side: str | None = None
182+
) -> list[Segment]:
183+
"""Parse a raw DNA match string and return a list of segments."""
161184
segments = []
162-
for line in note.get().split("\n"):
185+
for line in raw_string.split("\n"):
163186
data = parse_line(line, side=side)
164187
if data:
165188
segments.append(data)
166189
return segments
167190

168191

169-
def parse_line(line: str, side: Optional[str] = None) -> Optional[Segment]:
192+
def parse_line(line: str, side: str | None = None) -> Segment | None:
170193
"""Parse a line from the CSV/TSV data and return a dictionary."""
171194
if "\t" in line:
172195
# Tabs are the field separators. Now determine THOUSEP and RADIXCHAR.

gramps_webapi/data/apispec.yaml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
swagger: "2.0"
22
info:
33
title: "Gramps Web API"
4-
version: "2.7.0"
4+
version: "2.8.0"
55
description: >
66
The Gramps Web API is a REST API that provides access to family tree databases generated and maintained with Gramps, a popular Open Source genealogical research software package.
77
@@ -7128,6 +7128,37 @@ paths:
71287128
description: "Unauthorized: Missing authorization header."
71297129

71307130

7131+
##############################################################################
7132+
# Endpoint - Parsers
7133+
##############################################################################
7134+
7135+
/parsers/dna-match:
7136+
post:
7137+
tags:
7138+
- parsers
7139+
summary: "Parse a DNA match file."
7140+
operationId: parseDnaMatch
7141+
security:
7142+
- Bearer: []
7143+
parameters:
7144+
- name: string
7145+
in: json
7146+
required: true
7147+
type: string
7148+
description: "The raw DNA match data to parse."
7149+
responses:
7150+
200:
7151+
description: "OK: sucessfully parser."
7152+
schema:
7153+
type: array
7154+
items:
7155+
$ref: "#/definitions/DnaMatch"
7156+
401:
7157+
description: "Unauthorized: Missing authorization header."
7158+
422:
7159+
description: "Unprocessable Entity: Invalid or bad parameter provided."
7160+
7161+
71317162
##############################################################################
71327163
# Endpoint - Trees
71337164
##############################################################################

tests/test_endpoints/test_dna.py

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#
22
# Gramps Web API - A RESTful API for the Gramps genealogy program
33
#
4-
# Copyright (C) 2023 David Straub
4+
# Copyright (C) 2023-25 David Straub
55
#
66
# This program is free software; you can redistribute it and/or modify
77
# it under the terms of the GNU Affero General Public License as published by
@@ -39,14 +39,13 @@
3939
)
4040
from gramps_webapi.const import ENV_CONFIG_FILE, TEST_AUTH_CONFIG
4141

42-
43-
match1 = """chromosome,start,end,cMs,SNP
42+
MATCH1 = """chromosome,start,end,cMs,SNP
4443
1,56950055,64247327,10.9,1404
4544
5,850055,950055,12,1700
4645
"""
47-
match2 = """chromosome start end cMs SNP Side
46+
MATCH2 = """chromosome start end cMs SNP Side
4847
2 56950055 64247327 10.9 1404 M"""
49-
match3 = """chromosome,start,end,cMs
48+
MATCH3 = """chromosome,start,end,cMs
5049
X,56950055,64247327,10.9"""
5150

5251

@@ -62,13 +61,6 @@ def make_handle() -> str:
6261
return str(uuid.uuid4())
6362

6463

65-
def get_headers(client, user: str, password: str) -> Dict[str, str]:
66-
"""Get the auth headers for a specific user."""
67-
rv = client.post("/api/token/", json={"username": user, "password": password})
68-
access_token = rv.json["access_token"]
69-
return {"Authorization": "Bearer {}".format(access_token)}
70-
71-
7264
class TestDnaMatches(unittest.TestCase):
7365
@classmethod
7466
def setUpClass(cls):
@@ -127,7 +119,7 @@ def test_no_assoc(self):
127119
assert rv.json == []
128120

129121
def test_one(self):
130-
"""Test without association."""
122+
"""Full test."""
131123
headers = get_headers(self.client, "admin", "123")
132124
handle_p1 = make_handle()
133125
handle_p2 = make_handle()
@@ -255,17 +247,17 @@ def test_one(self):
255247
{
256248
"_class": "Note",
257249
"handle": handle_n1,
258-
"text": {"_class": "StyledText", "string": match1},
250+
"text": {"_class": "StyledText", "string": MATCH1},
259251
},
260252
{
261253
"_class": "Note",
262254
"handle": handle_n2,
263-
"text": {"_class": "StyledText", "string": match2},
255+
"text": {"_class": "StyledText", "string": MATCH2},
264256
},
265257
{
266258
"_class": "Note",
267259
"handle": handle_n3,
268-
"text": {"_class": "StyledText", "string": match3},
260+
"text": {"_class": "StyledText", "string": MATCH3},
269261
},
270262
]
271263
rv = self.client.post("/api/objects/", json=objects, headers=headers)
@@ -280,13 +272,6 @@ def test_one(self):
280272
assert data["ancestor_handles"] == [handle_grandf]
281273
assert data["relation"] == "le premier cousin"
282274
assert len(data["segments"]) == 4
283-
# 1,56950055,64247327,10.9,1404
284-
# 5,850055,950055,12,1700
285-
# """
286-
# match2 = """chromosome start end cMs SNP Side
287-
# 2 56950055 64247327 10.9 1404 M"""
288-
# match3 = """chromosome,start,end,cMs
289-
# X,56950055,64247327,10.9"""
290275
assert data["segments"][0] == {
291276
"chromosome": "1",
292277
"start": 56950055,
@@ -323,3 +308,16 @@ def test_one(self):
323308
"SNPs": 0,
324309
"comment": "",
325310
}
311+
# empty string
312+
rv = self.client.post(
313+
f"/api/parsers/dna-match", headers=headers, json={"string": ""}
314+
)
315+
assert rv.status_code == 200
316+
assert rv.json == []
317+
rv = self.client.post(
318+
f"/api/parsers/dna-match", headers=headers, json={"string": MATCH1}
319+
)
320+
assert rv.status_code == 200
321+
data = rv.json
322+
assert data
323+
assert len(data) == 2

0 commit comments

Comments
 (0)