Skip to content

Commit

Permalink
feat: switch from attrs to pydantic (#166) (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
holtgrewe authored Dec 11, 2023
1 parent e769243 commit d84db77
Show file tree
Hide file tree
Showing 33 changed files with 850 additions and 759 deletions.
40 changes: 17 additions & 23 deletions clinvar_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import json
import typing

import attrs
import cattrs
from jsonschema import ValidationError
from logzero import logger
from pydantic import BaseModel, SecretStr
from pydantic.config import ConfigDict
import requests

from clinvar_api import common, exceptions, models, msg, schemas
Expand All @@ -21,20 +21,13 @@
SUFFIX_DRYRUN = "?dry-run=true"


def _obfuscate_repr(s):
"""Helper function for obfustating passwords"""
if len(s) < 5:
return repr("*" * len(s))
else:
return repr(s[:5] + "*" * (len(s) - 5))


@attrs.define(frozen=True)
class Config:
class Config(BaseModel):
"""Configuration for the ``Client`` class."""

model_config = ConfigDict(frozen=True)

#: Token to use for authentication.
auth_token: str = attrs.field(repr=_obfuscate_repr)
auth_token: SecretStr

#: Whether to use the test endpoint.
use_testing: bool = False
Expand Down Expand Up @@ -64,10 +57,10 @@ def submit_data(submission_container: models.SubmissionContainer, config: Config
url = f"{url_prefix}{url_suffix}"
logger.debug("Will submit to URL %s", url)
headers = {
"SP-API-KEY": config.auth_token,
"SP-API-KEY": config.auth_token.get_secret_value(),
}

payload = cattrs.unstructure(submission_container.to_msg())
payload = submission_container.to_msg().model_dump(mode="json")
logger.debug("Payload data is %s", json.dumps(payload, indent=2))
cleaned_payload = common.clean_for_json(payload)
logger.debug("Cleaned payload data is %s", json.dumps(cleaned_payload, indent=2))
Expand All @@ -93,11 +86,11 @@ def submit_data(submission_container: models.SubmissionContainer, config: Config
logger.info("Server returned '204: No Content', constructing fake created message.")
return models.Created(id="--NONE--dry-run-result--")
else:
created_msg = common.CONVERTER.structure(response.json(), msg.Created)
created_msg = msg.Created.model_validate_json(response.content)
return models.Created.from_msg(created_msg)
else:
logger.warning("API returned an error - %s: %s", response.status_code, response.reason)
error_msg = common.CONVERTER.structure(response.json(), msg.Error)
error_msg = msg.Error.model_validate_json(response.content)
error_obj = models.Error.from_msg(error_msg)
logger.debug("Full server response is %s", response.json())
if hasattr(error_obj, "errors"):
Expand All @@ -108,10 +101,11 @@ def submit_data(submission_container: models.SubmissionContainer, config: Config
raise exceptions.SubmissionFailed(f"ClinVar submission failed: {error_obj.message}")


@attrs.define(frozen=True)
class RetrieveStatusResult:
class RetrieveStatusResult(BaseModel):
"""Result type for ``retrieve_status`` function."""

model_config = ConfigDict(frozen=True)

#: The submission status.
status: models.SubmissionStatus
#: A dict mapping file URLs to the parsed ``Sum``.
Expand All @@ -132,7 +126,7 @@ def _retrieve_status_summary(
except ValidationError as e:
logger.warning("Response summary validation JSON is invalid: %s", e)
logger.debug("... done validating status summary response")
sr_msg = cattrs.structure(response.json(), msg.SummaryResponse)
sr_msg = msg.SummaryResponse.model_validate_json(response.content)
return models.SummaryResponse.from_msg(sr_msg)
else:
raise exceptions.QueryFailed(
Expand All @@ -155,17 +149,17 @@ def retrieve_status(
url_suffix = SUFFIX_DRYRUN if config.use_dryrun else ""
url = f"{url_prefix}{submission_id}/actions/{url_suffix}"
headers = {
"SP-API-KEY": config.auth_token,
"SP-API-KEY": config.auth_token.get_secret_value(),
}
logger.debug("Will query URL %s", url)
response = requests.get(url, headers=headers)
if response.ok:
logger.info("API returned OK - %s: %s", response.status_code, response.reason)
logger.debug("Structuring response ...")
status_msg = common.CONVERTER.structure(response.json(), msg.SubmissionStatus)
status_msg = msg.SubmissionStatus.model_validate_json(response.content)
logger.debug(
"structured response is %s",
json.dumps(common.CONVERTER.unstructure(status_msg), indent=2),
status_msg.model_dump_json(indent=2),
)
logger.debug("... done structuring response")
status_obj = models.SubmissionStatus.from_msg(status_msg)
Expand Down
25 changes: 0 additions & 25 deletions clinvar_api/common.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,4 @@
import datetime
import typing
import uuid

import cattr
import dateutil.parser


def _setup_converter() -> cattr.Converter:
"""Setup ``cattr`` converter for UUID and datetime."""
result = cattr.Converter()
result.register_structure_hook(uuid.UUID, lambda d, _: uuid.UUID(d))
result.register_unstructure_hook(uuid.UUID, str)
result.register_structure_hook(datetime.datetime, lambda d, _: dateutil.parser.parse(d))
result.register_unstructure_hook(
datetime.datetime,
lambda obj: obj.replace(tzinfo=datetime.timezone.utc)
.astimezone()
.replace(microsecond=0)
.isoformat(),
)
return result


#: cattr Converter to use
CONVERTER = _setup_converter()


def clean_for_json(
Expand Down
38 changes: 23 additions & 15 deletions clinvar_api/models/query_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,26 @@
import datetime
import typing

import attrs
from pydantic import BaseModel
from pydantic.config import ConfigDict

from clinvar_api import msg
from clinvar_api.msg import ErrorCode


@attrs.define(frozen=True)
class SubmissionStatusFile:
class SubmissionStatusFile(BaseModel):
model_config = ConfigDict(frozen=True)

url: str

@classmethod
def from_msg(cls, other: msg.SubmissionStatusFile):
return SubmissionStatusFile(url=other.url)


@attrs.define(frozen=True)
class SubmissionStatusObjectContent:
class SubmissionStatusObjectContent(BaseModel):
model_config = ConfigDict(frozen=True)

#: Processing status
clinvar_processing_status: str
#: Release status
Expand All @@ -33,8 +36,9 @@ def from_msg(cls, other: msg.SubmissionStatusObjectContent):
)


@attrs.define(frozen=True)
class SubmissionStatusObject:
class SubmissionStatusObject(BaseModel):
model_config = ConfigDict(frozen=True)

#: Optional object accession.
accession: typing.Optional[str]
#: Object content.
Expand All @@ -51,8 +55,9 @@ def from_msg(cls, other: msg.SubmissionStatusObject):
)


@attrs.define(frozen=True)
class SubmissionStatusResponseMessage:
class SubmissionStatusResponseMessage(BaseModel):
model_config = ConfigDict(frozen=True)

#: The error code.
error_code: typing.Optional[ErrorCode]
#: The message severity.
Expand All @@ -67,8 +72,9 @@ def from_msg(cls, other: msg.SubmissionStatusResponseMessage):
)


@attrs.define(frozen=True)
class SubmissionStatusResponse:
class SubmissionStatusResponse(BaseModel):
model_config = ConfigDict(frozen=True)

#: Status, one of "processing", "processed", "error",
status: str
#: Files in the response.
Expand All @@ -92,8 +98,9 @@ def from_msg(cls, other: msg.SubmissionStatusResponse):
)


@attrs.define(frozen=True)
class SubmissionStatusActions:
class SubmissionStatusActions(BaseModel):
model_config = ConfigDict(frozen=True)

#: Identifier of the submission
id: str
#: Entries in ``actions[*].responses``, only one entry per the docs.
Expand All @@ -116,10 +123,11 @@ def from_msg(cls, other: msg.SubmissionStatusActions):
)


@attrs.define(frozen=True)
class SubmissionStatus:
class SubmissionStatus(BaseModel):
"""Internal submission status."""

model_config = ConfigDict(frozen=True)

#: The list of actions (one element only by the docs).
actions: typing.List[SubmissionStatusActions]

Expand Down
Loading

0 comments on commit d84db77

Please sign in to comment.