Skip to content

Commit

Permalink
Merge pull request #48 from oslokommune/stdlib-dataclasses
Browse files Browse the repository at this point in the history
Use standard lib dataclasses instead of Pydantic
  • Loading branch information
petterhj authored Mar 18, 2024
2 parents 083ad4c + f36d36e commit 1718621
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.2.0
current_version = 3.0.0
commit = True
tag = True

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 3.0.0 - 2024-03-18

* Refactored status data helpers to adopt standard dataclasses in place of
Pydantic, preventing Pydantic from being included as a transitive dependency
for projects utilizing these helpers.

## 2.2.0 - 2024-03-18

* `okdata.aws.status.sdk.Status` now accepts an additional optional
Expand Down
81 changes: 53 additions & 28 deletions okdata/aws/status/model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from enum import Enum
from json import dumps, JSONEncoder
from typing import Dict, Optional, List
from datetime import datetime, timezone
from pydantic import BaseModel, Field, validator


class TraceStatus(str, Enum):
Expand All @@ -15,6 +16,35 @@ class TraceEventStatus(str, Enum):
FAILED = "FAILED"


class StatusJSONEncoder(JSONEncoder):
def default(self, obj, *args, **kwargs):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)


class BaseModel:
def dict(self, exclude_none=False):
if exclude_none:
return asdict(
self,
dict_factory=lambda d: {k: v for (k, v) in d if v is not None},
)
return asdict(self)

def json(self, exclude_none=False, **kwargs):
return dumps(
self.dict(exclude_none=exclude_none),
cls=StatusJSONEncoder,
**kwargs,
)

@classmethod
def parse_obj(cls, obj):
return cls(**obj)


@dataclass
class StatusMeta(BaseModel):
function_name: Optional[str] = None
function_version: Optional[str] = None
Expand All @@ -27,17 +57,14 @@ class StatusMeta(BaseModel):
# TODO: Rework optional vs required and defaults when currents users are updated
# and if to be used as a basis for the status api. Both trace_id (for new traces)
# and trace_event_id are currently generated in status-api.
@dataclass
class StatusData(BaseModel):
trace_id: Optional[str] = None # TODO: Generate here as default?
# trace_event_id: UUID = None # = Field(default_factory=uuid4)
domain: str = "N/A" # TODO: Temporary default (required)
domain_id: Optional[str] = None
start_time: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc), str=datetime.isoformat
)
end_time: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc), str=datetime.isoformat
)
start_time: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
end_time: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
trace_status: TraceStatus = TraceStatus.CONTINUE
trace_event_status: TraceEventStatus = TraceEventStatus.OK
user: Optional[str] = None
Expand All @@ -50,23 +77,21 @@ class StatusData(BaseModel):
exception: Optional[str] = None
errors: Optional[List] = None

class Config:
validate_assignment = True

@validator("exception", pre=True)
def ensure_exception_data_is_string(cls, v):
if isinstance(v, Exception):
return str(v)
return v

@validator("errors", each_item=True)
def ensure_format_of_errors(cls, v):
if not isinstance(v, dict):
raise TypeError(f"{v} is not a dict.")
if "message" not in v:
raise ValueError("Missing key 'message'.")
if not isinstance(v["message"], dict):
raise TypeError("error['message'] is not a dict.")
if "nb" not in v["message"]:
raise ValueError("Missing key 'nb' in error['message'].")
return v
def __post_init__(self):
# Ensure that `meta` is of type `StatusMeta` if provided as a dictionary.
if isinstance(self.meta, dict):
self.meta = StatusMeta(**self.meta)

# Ensure that exception data is a string
self.exception = str(self.exception) if self.exception else None

# Validate and ensure format of errors
for error in self.errors or []:
if not isinstance(error, dict):
raise TypeError(f"{error} is not a dict.")
if "message" not in error:
raise ValueError("Missing key 'message'.")
if not isinstance(error["message"], dict):
raise TypeError("error['message'] is not a dict.")
if "nb" not in error["message"]:
raise ValueError("Missing key 'nb' in error['message'].")
4 changes: 0 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ pyasn1==0.4.8
# via
# python-jose
# rsa
pydantic==1.8.2
# via okdata-aws (setup.py)
pyjwt==2.4.0
# via okdata-sdk
pyrsistent==0.18.0
Expand Down Expand Up @@ -69,8 +67,6 @@ starlette==0.36.2
# via okdata-aws (setup.py)
structlog==21.2.0
# via okdata-aws (setup.py)
typing-extensions==3.10.0.2
# via pydantic
urllib3==1.26.18
# via
# botocore
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="okdata-aws",
version="2.2.0",
version="3.0.0",
author="Oslo Origo",
author_email="dataplattform@oslo.kommune.no",
description="Collection of helpers for working with AWS",
Expand All @@ -30,7 +30,6 @@
install_requires=[
"boto3",
"okdata-sdk>=3,<4",
"pydantic<2",
"requests",
"starlette>=0.25.0,<1.0.0",
"structlog",
Expand Down
3 changes: 1 addition & 2 deletions tests/test_model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import pytest
from pydantic.error_wrappers import ValidationError

from okdata.aws.status.model import StatusData

Expand All @@ -15,7 +14,7 @@ def test_errors_entry_valid(self):

def test_errors_entry_not_dict(self):
params = {"errors": [OK_ERROR, "This is string, not a dict."]}
with pytest.raises(ValidationError):
with pytest.raises(TypeError):
StatusData(**params)

def test_errors_entry_no_message(self):
Expand Down

0 comments on commit 1718621

Please sign in to comment.