Skip to content

Commit

Permalink
fixup! feat(sdk): upgrade to Pydantic v2
Browse files Browse the repository at this point in the history
  • Loading branch information
IamAbbey committed Jul 17, 2023
1 parent 97975e6 commit 26492ca
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 259 deletions.
275 changes: 136 additions & 139 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/responses/issue.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"$type": "SingleEnumIssueCustomField"
},
{
"value": 1645110000000,
"value": 1688472000000,
"projectCustomField": {
"field": {
"fieldType": {
Expand Down
2 changes: 1 addition & 1 deletion tests/responses/issues.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"$type": "SingleEnumIssueCustomField"
},
{
"value": 1645110000000,
"value": 1688472000000,
"projectCustomField": {
"field": {
"fieldType": {
Expand Down
2 changes: 1 addition & 1 deletion tests/responses/sprints.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
"$type": "SingleEnumIssueCustomField"
},
{
"value": 1645110000000,
"value": 1688472000000,
"projectCustomField": {
"field": {
"fieldType": {
Expand Down
11 changes: 6 additions & 5 deletions tests/test_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,14 @@
UserGroup,
UserProjectCustomField,
)
from youtrack_sdk.types import TimeZoneDateTime


class CustomIssue(BaseModel):
type: Literal["Issue"] = Field(alias="$type", default="Issue")
id_readable: Optional[str] = Field(alias="idReadable", default=None)
created: Optional[TimeZoneDateTime] = None
updated: Optional[TimeZoneDateTime] = None
resolved: Optional[TimeZoneDateTime] = None
created: Optional[datetime] = None
updated: Optional[datetime] = None
resolved: Optional[datetime] = None
comments_count: Optional[int] = Field(alias="commentsCount", default=None)
custom_fields: Optional[Sequence[IssueCustomFieldType]] = Field(alias="customFields", default=None)

Expand All @@ -61,13 +60,15 @@ class CustomIssue(BaseModel):
ring_id="b0fea1e1-ed18-43f6-a99d-40044fb1dfb0",
login="support",
email="[email protected]",
name=None,
),
updater=User.model_construct(
type="User",
id="1-17",
ring_id="c5d08431-dd52-4cdd-9911-7ec3a18ad117",
login="max.demo",
email="[email protected]",
name=None,
),
summary="Summary text",
description="Issue description",
Expand Down Expand Up @@ -136,7 +137,7 @@ class CustomIssue(BaseModel):
id="145-34",
name="Due Date",
type="DateIssueCustomField",
value=date(2022, 2, 17),
value=date(2023, 7, 4),
project_custom_field=SimpleProjectCustomField.model_construct(
field=CustomField.model_construct(
type="CustomField",
Expand Down
30 changes: 14 additions & 16 deletions youtrack_sdk/entities.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from datetime import datetime
from datetime import UTC, datetime
from typing import Literal, Optional, Sequence

from pydantic import BaseModel as PydanticBaseModel
from pydantic import ConfigDict, Field, StrictFloat, StrictInt, StrictStr, field_validator
from pydantic_core.core_schema import FieldValidationInfo

from .exceptions import StrictIntError
from .helpers import from_unix_seconds
from .types import CustomDate, TimeZoneDateTime
from .types import UnixMidDayDate


class BaseModel(PydanticBaseModel):
Expand Down Expand Up @@ -170,16 +168,16 @@ def validate_value(cls, value, info: FieldValidationInfo):
return value

if not isinstance(value, int):
raise StrictIntError
raise ValueError("Expects value to be int")

return from_unix_seconds(value)
return datetime.fromtimestamp(value / 1000, UTC)

return value


class DateIssueCustomField(SimpleIssueCustomField):
type: Literal["DateIssueCustomField"] = Field(alias="$type", default="DateIssueCustomField")
value: Optional[CustomDate]
value: Optional[UnixMidDayDate]


class PeriodIssueCustomField(IssueCustomField):
Expand Down Expand Up @@ -290,9 +288,9 @@ class Issue(BaseModel):
type: Literal["Issue"] = Field(alias="$type", default="Issue")
id: Optional[str] = None
id_readable: Optional[str] = Field(alias="idReadable", default=None)
created: Optional[TimeZoneDateTime] = None
updated: Optional[TimeZoneDateTime] = None
resolved: Optional[TimeZoneDateTime] = None
created: Optional[datetime] = None
updated: Optional[datetime] = None
resolved: Optional[datetime] = None
project: Optional[Project] = None
reporter: Optional[User] = None
updater: Optional[User] = None
Expand All @@ -309,8 +307,8 @@ class IssueAttachment(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
author: Optional[User] = None
created: Optional[TimeZoneDateTime] = None
updated: Optional[TimeZoneDateTime] = None
created: Optional[datetime] = None
updated: Optional[datetime] = None
mime_type: Optional[str] = Field(alias="mimeType", default=None)
url: Optional[str] = None

Expand All @@ -320,8 +318,8 @@ class IssueComment(BaseModel):
id: Optional[str] = None
text: Optional[str] = None
text_preview: Optional[str] = Field(alias="textPreview", default=None)
created: Optional[TimeZoneDateTime] = None
updated: Optional[TimeZoneDateTime] = None
created: Optional[datetime] = None
updated: Optional[datetime] = None
author: Optional[User] = None
attachments: Optional[Sequence[IssueAttachment]] = None
deleted: Optional[bool] = None
Expand Down Expand Up @@ -372,8 +370,8 @@ class Agile(AgileRef):
class Sprint(SprintRef):
agile: Optional[AgileRef] = None
goal: Optional[str] = None
start: Optional[TimeZoneDateTime] = None
finish: Optional[TimeZoneDateTime] = None
start: Optional[datetime] = None
finish: Optional[datetime] = None
archived: Optional[bool] = None
is_default: Optional[bool] = Field(alias="isDefault", default=None)
issues: Optional[Sequence[Issue]] = None
Expand Down
17 changes: 0 additions & 17 deletions youtrack_sdk/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from pydantic.errors import PydanticUserError


class YouTrackException(Exception):
pass

Expand All @@ -11,17 +8,3 @@ class YouTrackNotFound(YouTrackException):

class YouTrackUnauthorized(YouTrackException):
pass


class StrictIntError(PydanticUserError):
message = "value is not a valid integer"

def __init__(self) -> None:
super().__init__(self.message, code=None)


class IncompatibleFieldTypeError(PydanticUserError):
message = "incompatible field type"

def __init__(self) -> None:
super().__init__(self.message, code=None)
22 changes: 1 addition & 21 deletions youtrack_sdk/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from copy import deepcopy
from datetime import UTC, date, datetime, time, timedelta
from datetime import UTC, date, datetime, time
from itertools import starmap
from typing import Any, Optional, Type, Union, get_args

Expand Down Expand Up @@ -114,23 +114,3 @@ def custom_json_dumps(obj: Any) -> str:

def obj_to_json(obj: Optional[BaseModel]) -> str:
return custom_json_dumps(obj_to_dict(obj))


def from_unix_seconds(seconds: Union[int, float]) -> datetime:
EPOCH = datetime(1970, 1, 1)
# if greater than this, the number is in ms, if less than or equal it's in seconds
# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
MS_WATERSHED = int(2e10)
# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
MAX_NUMBER = int(3e20)

if seconds > MAX_NUMBER:
return datetime.max
elif seconds < -MAX_NUMBER:
return datetime.min

while abs(seconds) > MS_WATERSHED:
seconds /= 1000
dt = EPOCH + timedelta(seconds=seconds)

return dt.replace(tzinfo=UTC)
67 changes: 9 additions & 58 deletions youtrack_sdk/types.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,15 @@
from datetime import UTC, date, datetime
from datetime import UTC, date, datetime, timedelta
from enum import StrEnum
from typing import Any
from typing import Annotated

from pydantic._internal import _annotated_handlers
from pydantic_core import core_schema
from pydantic import BeforeValidator

from .exceptions import StrictIntError
from .helpers import from_unix_seconds


class CustomDate:
"""A custom date implementation that does not raise Pydantic's `date_from_datetime_inexact`.
For further information visit https://errors.pydantic.dev/2.0.1/v/date_from_datetime_inexact"""

@staticmethod
def _validate(value: Any, _: core_schema.ValidationInfo) -> date:
if isinstance(value, date):
return value

if not isinstance(value, int):
raise StrictIntError

return from_unix_seconds(value).date()

@classmethod
def __get_pydantic_core_schema__(
cls,
_: type[Any],
__: _annotated_handlers.GetCoreSchemaHandler,
) -> core_schema.CoreSchema:
return core_schema.general_before_validator_function(
function=cls._validate,
schema=core_schema.date_schema(),
)

def __repr__(self) -> str:
return "CustomDate"


DEFAULT_TIME_ZONE = UTC


class TimeZoneDateTime(datetime):
"""A custom datetime implementation that returns datetime with tzinfo."""

@staticmethod
def _validate(
v: str,
validator: core_schema.ValidatorFunctionWrapHandler,
_: core_schema.ValidationInfo,
) -> datetime:
return validator(input_value=v).replace(tzinfo=DEFAULT_TIME_ZONE)

@classmethod
def __get_pydantic_core_schema__(
cls,
_: type[Any],
__: _annotated_handlers.GetCoreSchemaHandler,
) -> core_schema.CoreSchema:
return core_schema.general_wrap_validator_function(cls._validate, core_schema.datetime_schema())
UnixMidDayDate = Annotated[
date,
BeforeValidator(
lambda d: (datetime.fromtimestamp(d / 1000, UTC) - timedelta(hours=12)) if isinstance(d, int) else d,
),
]


class IssueLinkDirection(StrEnum):
Expand Down

0 comments on commit 26492ca

Please sign in to comment.