Skip to content

Commit

Permalink
Merge pull request #7 from ahida-development/pydantic2
Browse files Browse the repository at this point in the history
Breaking change: Update to pydantic v2
  • Loading branch information
marius-mather authored Jul 27, 2023
2 parents 0147ea7 + 73b1d22 commit 067ed2b
Show file tree
Hide file tree
Showing 11 changed files with 818 additions and 614 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Changed
- Updated to [Pydantic V2](https://docs.pydantic.dev/latest/): the new version has useful features such as multiple aliases for fields
- In Pydantic V2, url fields are stored as a URL class and cannot be directly used as strings - use `str(model.url_field)` to use them as string.
### Fixed
- Updated some schema fields to reflect latest changes to OLS4 - still a bit of a moving target!

## [0.3.0] - 2023-06-05
### Added
Expand Down Expand Up @@ -99,4 +104,3 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
[0.1.0]: https://github.com/ahida-development/ols-py/compare/0.0.3...0.1.0
[0.0.3]: https://github.com/ahida-development/ols-py/compare/0.0.2...0.0.3
[0.0.2]: https://github.com/ahida-development/ols-py/tree/0.0.2

1,275 changes: 733 additions & 542 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]
Expand All @@ -30,7 +31,7 @@ packages = [
[tool.poetry.dependencies]
python = ">=3.10.1, <4.0"
requests = ">=2.0, <3.0"
pydantic = "^1.10.2"
pydantic = "^2.1.1"

[tool.poetry.group.jupyterlab]
optional = true
Expand Down
2 changes: 1 addition & 1 deletion src/ols_py/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def get_ontologies(
:param page: Page number of results (starting at 0)
:param size: Number of results per page (API default is 20)
"""
params = schemas.requests.PageParams(page=page, size=size).dict(
params = schemas.requests.PageParams(page=page, size=size).model_dump(
exclude_none=True
)
ontology_list = self.get_with_schema(
Expand Down
34 changes: 19 additions & 15 deletions src/ols_py/ols4_schemas/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,32 @@

from typing import Optional

from pydantic import BaseModel, Extra, Field
from pydantic import AliasChoices, BaseModel, Field

from ..schemas import responses
from ..schemas.common import EntityType


# Structure of search results has changed from OLS v3,
# some fields are now list[str] instead of str
class SearchResultItem(BaseModel, extra=Extra.allow):
id: Optional[str]
annotations: Optional[list[str]]
annotations_trimmed: Optional[list[str]]
description: Optional[list[str]]
iri: Optional[str]
label: Optional[list[str]]
obo_id: Optional[list[str]]
ontology_name: Optional[str]
ontology_prefix: Optional[str]
subset: Optional[list[str]]
short_form: Optional[list[str]]
synonym: Optional[list[str]]
type: Optional[EntityType]
class SearchResultItem(BaseModel, extra="allow"):
id: Optional[str] = None
annotations: Optional[list[str]] = None
annotations_trimmed: Optional[list[str]] = None
description: Optional[list[str]] = None
iri: Optional[str] = None
label: Optional[str] = None
obo_id: Optional[str] = None
ontology_name: Optional[str] = None
ontology_prefix: Optional[str] = None
subset: Optional[list[str]] = None
short_form: Optional[str] = None
# OLS4 has recently switched to "synonyms" for this field,
# where OLS3 has "synonym"
synonyms: Optional[list[str]] = Field(
default=None, validation_alias=AliasChoices("synonyms", "synonym")
)
type: Optional[EntityType] = None


class SearchResponseResponse(BaseModel):
Expand Down
5 changes: 3 additions & 2 deletions src/ols_py/schemas/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from typing import TYPE_CHECKING, Literal

from pydantic import constr
from pydantic import StringConstraints
from typing_extensions import Annotated

EntityType = Literal["class", "property", "individual", "ontology"]

Expand All @@ -11,4 +12,4 @@
if TYPE_CHECKING:
AnnotationFieldName = str
else:
AnnotationFieldName = constr(regex=r"^\w+_annotation$")
AnnotationFieldName = Annotated[str, StringConstraints(pattern=r"^\w+_annotation$")]
40 changes: 22 additions & 18 deletions src/ols_py/schemas/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Literal, Optional, TypedDict

from pydantic import BaseModel, NonNegativeInt, PositiveInt, validator
from pydantic import BaseModel, NonNegativeInt, PositiveInt, field_validator

from .common import AnnotationFieldName, EntityType

Expand All @@ -13,9 +13,9 @@ class PageParams(BaseModel):
resources
"""

size: Optional[PositiveInt]
size: Optional[PositiveInt] = None
"""Number of results per page"""
page: Optional[NonNegativeInt]
page: Optional[NonNegativeInt] = None
"""Which page to fetch (starting at 0)"""


Expand All @@ -35,7 +35,10 @@ class PageParams(BaseModel):
"ontology_prefix",
"short_form",
"subset",
# TODO: OLS3 uses synonym, OLS4 uses synonyms. Best option for now
# is to allow both.
"synonym",
"synonyms",
"type",
]
SearchQueryFields = Literal[
Expand All @@ -62,47 +65,48 @@ class SearchParams(BaseModel):

q: str
"""Query to search for"""
ontology: Optional[list[str]]
ontology: Optional[list[str]] = None
"""Ontologies to search, e.g. `["mondo", "upheno"]`"""
type: Optional[EntityType]
type: Optional[EntityType] = None
"""Type of term to search for, e.g. "class", "property" """
slim: Optional[list[str]]
fieldList: Optional[list[SearchReturnFields | AnnotationFieldName]]
slim: Optional[list[str]] = None
fieldList: Optional[list[SearchReturnFields | AnnotationFieldName]] = None
"""Which fields to return in the results"""
queryFields: Optional[list[SearchQueryFields | AnnotationFieldName]]
queryFields: Optional[list[SearchQueryFields | AnnotationFieldName]] = None
"""Which fields to search over"""
exact: Optional[bool]
groupField: Optional[bool]
exact: Optional[bool] = None
groupField: Optional[bool] = None
"""Group results by unique ID"""
obsoletes: Optional[bool]
obsoletes: Optional[bool] = None
"""Include obsoleted terms in the results"""
local: Optional[bool]
local: Optional[bool] = None
"""Only return terms in a defining ontology"""
childrenOf: Optional[list[str]]
childrenOf: Optional[list[str]] = None
"""Restrict results to children of these terms"""
allChildrenOf: Optional[list[str]]
allChildrenOf: Optional[list[str]] = None
"""
Restrict results to children of these terms, plus other
child-like relations e.g. "part of", "develops from"
"""
rows: Optional[int]
rows: Optional[int] = None
"""
Number of results per page
"""
start: Optional[int]
start: Optional[int] = None
"""
Index of first result
"""

@validator(
@field_validator(
"ontology",
"slim",
"childrenOf",
"allChildrenOf",
"fieldList",
"queryFields",
pre=True,
mode="before",
)
@classmethod
def _single_to_list(cls, v):
"""
If a single string is passed but we expect
Expand Down
46 changes: 21 additions & 25 deletions src/ols_py/schemas/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Any, Optional

import pydantic
from pydantic import BaseModel, Extra, Field, HttpUrl
from pydantic import BaseModel, ConfigDict, Field, HttpUrl

from ols_py.schemas.common import EntityType

Expand Down Expand Up @@ -38,23 +38,21 @@ class Term(BaseModel):
# Not specifying annotations for now, not sure if these
# are fixed
annotation: dict[str, list[str]]
synonyms: Optional[list[str]]
synonyms: Optional[list[str]] = None
ontology_name: str
ontology_prefix: str
ontology_iri: pydantic.AnyUrl
is_obsolete: bool
term_replaced_by: Optional[Any]
term_replaced_by: Optional[Any] = None
has_children: bool
is_root: bool
short_form: str
# Higher level terms may not have an obo ID, e.g.
# terms will be descendants of http://www.w3.org/2002/07/owl#Thing
obo_id: Optional[str]
in_subset: Optional[Any]
obo_id: Optional[str] = None
in_subset: Optional[Any] = None
links: dict[str, Link] = Field(..., alias="_links")

class Config:
extra = "allow"
model_config = ConfigDict(extra="allow")


class ApiInfoLinks(BaseModel):
Expand Down Expand Up @@ -84,9 +82,7 @@ class OntologyItem(BaseModel):
status: str
numberOfProperties: int
numberOfTerms: int

class Config:
extra = "allow"
model_config = ConfigDict(extra="allow")


class OntologyListEmbedded(BaseModel):
Expand Down Expand Up @@ -137,20 +133,20 @@ class TermInDefiningOntology(BaseModel):
page: PageInfo


class SearchResultItem(BaseModel, extra=Extra.allow):
id: Optional[str]
annotations: Optional[list[str]]
annotations_trimmed: Optional[list[str]]
description: Optional[list[str]]
iri: Optional[str]
label: Optional[str]
obo_id: Optional[str]
ontology_name: Optional[str]
ontology_prefix: Optional[str]
subset: Optional[list[str]]
short_form: Optional[str]
synonym: Optional[list[str]]
type: Optional[EntityType]
class SearchResultItem(BaseModel, extra="allow"):
id: Optional[str] = None
annotations: Optional[list[str]] = None
annotations_trimmed: Optional[list[str]] = None
description: Optional[list[str]] = None
iri: Optional[str] = None
label: Optional[str] = None
obo_id: Optional[str] = None
ontology_name: Optional[str] = None
ontology_prefix: Optional[str] = None
subset: Optional[list[str]] = None
short_form: Optional[str] = None
synonym: Optional[list[str]] = None
type: Optional[EntityType] = None


class SearchResponseResponse(BaseModel):
Expand Down
6 changes: 3 additions & 3 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_get_term(ebi_client):
ontology_id="go", iri="http://purl.obolibrary.org/obo/GO_0043226"
)
assert term.ontology_name == "go"
assert term.iri == "http://purl.obolibrary.org/obo/GO_0043226"
assert str(term.iri) == "http://purl.obolibrary.org/obo/GO_0043226"
assert term.label == "organelle"


Expand Down Expand Up @@ -175,11 +175,11 @@ def test_get_term_in_defining_ontology(ebi_client):
iri = "http://purl.obolibrary.org/obo/MONDO_0018660"
resp = ebi_client.get_term_in_defining_ontology(iri=iri)
term = resp.embedded.terms[0]
assert term.iri == iri
assert str(term.iri) == iri
assert term.ontology_name == "mondo"
# Should also allow searching by OBO ID etc. by passing params
obo_id = "MONDO:0018660"
resp_from_obo = ebi_client.get_term_in_defining_ontology(params={"obo_id": obo_id})
term_from_obo = resp_from_obo.embedded.terms[0]
assert term_from_obo.iri == iri
assert str(term_from_obo.iri) == iri
assert term_from_obo.ontology_name == "mondo"
8 changes: 4 additions & 4 deletions tests/test_ols4_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ def test_search_returns_synonyms(ols4_client):
"ontology": "ncbitaxon",
"rows": 10,
"queryFields": ["label"],
"fieldList": ["iri", "label", "synonym"],
"fieldList": ["iri", "label", "obo_id", "synonyms"],
# Use exact, we just want to make sure we get bos taurus so
# we can check its synonyms
"exact": True,
"childrenOf": ["http://purl.obolibrary.org/obo/NCBITaxon_9903"],
},
)
first_result = resp.response.docs[0]
assert "cow" in first_result.synonym
assert "cow" in first_result.synonyms


def test_get_term_in_defining_ontology(ols4_client):
Expand All @@ -51,7 +51,7 @@ def test_get_term_in_defining_ontology(ols4_client):
iri = "http://purl.obolibrary.org/obo/MONDO_0018660"
resp = ols4_client.get_term_in_defining_ontology(iri=iri)
term = resp.embedded.terms[0]
assert term.iri == iri
assert str(term.iri) == iri
assert term.ontology_name == "mondo"
# OBO ID search should be working now
obo_id = "MONDO:0018660"
Expand All @@ -64,4 +64,4 @@ def test_get_term_in_defining_ontology(ols4_client):
params={"short_form": short_form}
)
assert resp_from_short_form.page.totalElements == 1
assert resp_from_short_form.embedded.terms[0].iri == iri
assert str(resp_from_short_form.embedded.terms[0].iri) == iri
7 changes: 5 additions & 2 deletions tests/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ def test_search_fields():
"""
return_fields = set(get_args(ols_py.schemas.requests.SearchReturnFields))
result_item_fields = set(
ols_py.schemas.responses.SearchResultItem.schema()["properties"].keys()
ols_py.schemas.responses.SearchResultItem.model_json_schema()[
"properties"
].keys()
)
# Result items have an extra id field
assert result_item_fields - return_fields == {"id"}
assert return_fields - result_item_fields == set()
# TODO: currently have to allow for "synonyms" from OLS4
assert return_fields - result_item_fields == {"synonyms"}

0 comments on commit 067ed2b

Please sign in to comment.