Skip to content
This repository has been archived by the owner on Dec 16, 2024. It is now read-only.

Commit

Permalink
attr -> pydantic (#154)
Browse files Browse the repository at this point in the history
Introduces breaking changes

---------

Co-authored-by: Daniel Stoops <[email protected]>
  • Loading branch information
Stoops-ML and Daniel Stoops authored Nov 27, 2024
1 parent 4aab12e commit 78fcbe9
Show file tree
Hide file tree
Showing 22 changed files with 1,597 additions and 1,187 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
python-version: ['3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ or conda::

$ conda install czml3 --channel conda-forge

czml3 requires Python >= 3.7.
czml3 requires Python >= 3.8.

Examples
========
Expand Down
22 changes: 9 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.ruff.lint]
ignore = ["E203", "E266", "E501"]
ignore = ["E501"]
select = [
"E", # pycodestyle
"F", # Pyflakes
Expand All @@ -13,27 +13,23 @@ select = [
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = quality, test, pypy, pypy3, py{37,38,39,310,311,312}
envlist = quality, test, pypy, pypy3, py{310,311,312,313}
[gh-actions]
python =
3.7: py37
3.8: py38
3.9: py39
3.10: py310
3.11: py311, quality, test, pypy, pypy3
3.12: py312
3.13: py313
[testenv]
basepython =
pypy: {env:PYTHON:pypy}
pypy3: {env:PYTHON:pypy3}
py37: {env:PYTHON:python3.7}
py38: {env:PYTHON:python3.8}
py39: {env:PYTHON:python3.9}
py310: {env:PYTHON:python3.10}
py311: {env:PYTHON:python3.11}
py312: {env:PYTHON:python3.12}
py313: {env:PYTHON:python3.13}
{quality,reformat,test,coverage}: {env:PYTHON:python3}
setenv =
PYTHONUNBUFFERED = yes
Expand Down Expand Up @@ -79,7 +75,7 @@ authors = [
]
description = "Python 3 library to write CZML"
readme = "README.rst"
requires-python = ">=3.7"
requires-python = ">=3.10"
keywords = ["czml", "cesium", "orbits"]
license = {text = "MIT"}
classifiers = [
Expand All @@ -89,21 +85,21 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Physics",
"Topic :: Scientific/Engineering :: Astronomy",
]
dependencies = [
"attrs>=19.2",
"pydantic>=2.10.1",
"python-dateutil>=2.7,<3",
"w3lib",
"typing-extensions>=4.12.0",
"StrEnum>=0.4.0",
]
dynamic = ["version"]

Expand Down
2 changes: 1 addition & 1 deletion src/czml3/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .core import CZML_VERSION, Document, Packet, Preamble

__version__ = "1.0.2"
__version__ = "2.0.0"

__all__ = ["Document", "Preamble", "Packet", "CZML_VERSION"]
66 changes: 19 additions & 47 deletions src/czml3/base.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,27 @@
import datetime as dt
import json
import warnings
from enum import Enum
from json import JSONEncoder
from typing import Any

import attr

from .constants import ISO8601_FORMAT_Z
from pydantic import BaseModel, model_validator

NON_DELETE_PROPERTIES = ["id", "delete"]


class CZMLEncoder(JSONEncoder):
def default(self, o):
if isinstance(o, BaseCZMLObject):
return o.to_json()

elif isinstance(o, Enum):
return o.name

elif isinstance(o, dt.datetime):
return o.astimezone(dt.timezone.utc).strftime(ISO8601_FORMAT_Z)

return super().default(o)


@attr.s(str=False, frozen=True)
class BaseCZMLObject:
def __str__(self):
return self.dumps(indent=4)

def dumps(self, *args, **kwargs):
if "cls" in kwargs:
warnings.warn("Ignoring specified cls", UserWarning, stacklevel=2)

kwargs["cls"] = CZMLEncoder
return json.dumps(self, *args, **kwargs)

def dump(self, fp, *args, **kwargs):
for chunk in CZMLEncoder(*args, **kwargs).iterencode(self):
fp.write(chunk)
class BaseCZMLObject(BaseModel):
@model_validator(mode="before")
@classmethod
def check_model_before(cls, data: dict[str, Any]) -> Any:
if data is not None and "delete" in data and data["delete"]:
return {
"delete": True,
"id": data.get("id"),
**{k: None for k in data if k not in NON_DELETE_PROPERTIES},
}
return data

def to_json(self):
if getattr(self, "delete", False):
properties_list = NON_DELETE_PROPERTIES
else:
properties_list = list(attr.asdict(self).keys())
def __str__(self) -> str:
return self.to_json()

obj_dict = {}
for property_name in properties_list:
if getattr(self, property_name, None) is not None:
obj_dict[property_name] = getattr(self, property_name)
def dumps(self) -> str:
return self.model_dump_json(exclude_none=True)

return obj_dict
def to_json(self, *, indent: int = 4) -> str:
return self.model_dump_json(exclude_none=True, indent=indent)
26 changes: 13 additions & 13 deletions src/czml3/common.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
# noinspection PyPep8Naming
from __future__ import annotations

import datetime as dt

import attr
from pydantic import BaseModel, field_validator

from .enums import InterpolationAlgorithms
from .types import format_datetime_like


@attr.s(str=False, frozen=True, kw_only=True)
class Deletable:
class Deletable(BaseModel):
"""A property whose value may be deleted."""

delete: bool | None = attr.ib(default=None)
delete: None | bool = None


# noinspection PyPep8Naming
@attr.s(str=False, frozen=True, kw_only=True)
class Interpolatable:
class Interpolatable(BaseModel):
"""A property whose value may be determined by interpolating.
The interpolation happens over provided time-tagged samples.
"""

epoch: dt.datetime | None = attr.ib(default=None)
interpolationAlgorithm: InterpolationAlgorithms | None = attr.ib(default=None)
interpolationDegree: int | None = attr.ib(default=None)
epoch: None | str | dt.datetime = None
interpolationAlgorithm: None | InterpolationAlgorithms = None
interpolationDegree: None | int = None

@field_validator("epoch")
@classmethod
def check(cls, e):
return format_datetime_like(e)
104 changes: 64 additions & 40 deletions src/czml3/core.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,88 @@
from typing import Any
from uuid import uuid4

import attr
from pydantic import Field, model_serializer

from czml3.types import StringValue

from .base import BaseCZMLObject
from .types import Sequence
from .properties import (
Billboard,
Box,
Clock,
Corridor,
Cylinder,
Ellipse,
Ellipsoid,
Label,
Model,
Orientation,
Path,
Point,
Polygon,
Polyline,
Position,
Rectangle,
Tileset,
ViewFrom,
Wall,
)
from .types import IntervalValue, Sequence, TimeInterval

CZML_VERSION = "1.0"


@attr.s(str=False, frozen=True, kw_only=True)
class Preamble(BaseCZMLObject):
"""The preamble packet."""

id = attr.ib(init=False, default="document")

version = attr.ib(default=CZML_VERSION)
name = attr.ib(default=None)
description = attr.ib(default=None)
clock = attr.ib(default=None)
id: str = Field(default="document")
version: str = Field(default=CZML_VERSION)
name: None | str = Field(default=None)
description: None | str = Field(default=None)
clock: None | Clock | IntervalValue = Field(default=None)


@attr.s(str=False, frozen=True, kw_only=True)
class Packet(BaseCZMLObject):
"""A CZML Packet.
See https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/Packet
for further information.
"""

id = attr.ib(factory=lambda: str(uuid4()))
delete = attr.ib(default=None)
name = attr.ib(default=None)
parent = attr.ib(default=None)
description = attr.ib(default=None)
availability = attr.ib(default=None)
properties = attr.ib(default=None)
position = attr.ib(default=None)
orientation = attr.ib(default=None)
viewFrom = attr.ib(default=None)
billboard = attr.ib(default=None)
box = attr.ib(default=None)
corridor = attr.ib(default=None)
cylinder = attr.ib(default=None)
ellipse = attr.ib(default=None)
ellipsoid = attr.ib(default=None)
label = attr.ib(default=None)
model = attr.ib(default=None)
path = attr.ib(default=None)
point = attr.ib(default=None)
polygon = attr.ib(default=None)
polyline = attr.ib(default=None)
rectangle = attr.ib(default=None)
tileset = attr.ib(default=None)
wall = attr.ib(default=None)
id: str = Field(default=str(uuid4()))
delete: None | bool = Field(default=None)
name: None | str = Field(default=None)
parent: None | str = Field(default=None)
description: None | str | StringValue = Field(default=None)
availability: None | TimeInterval | list[TimeInterval] | Sequence = Field(
default=None
)
properties: None | Any = Field(default=None)
position: None | Position = Field(default=None)
orientation: None | Orientation = Field(default=None)
viewFrom: None | ViewFrom = Field(default=None)
billboard: None | Billboard = Field(default=None)
box: None | Box = Field(default=None)
corridor: None | Corridor = Field(default=None)
cylinder: None | Cylinder = Field(default=None)
ellipse: None | Ellipse = Field(default=None)
ellipsoid: None | Ellipsoid = Field(default=None)
label: None | Label = Field(default=None)
model: None | Model = Field(default=None)
path: None | Path = Field(default=None)
point: None | Point = Field(default=None)
polygon: None | Polygon = Field(default=None)
polyline: None | Polyline = Field(default=None)
rectangle: None | Rectangle = Field(default=None)
tileset: None | Tileset = Field(default=None)
wall: None | Wall = Field(default=None)


@attr.s(str=False, frozen=True)
class Document(Sequence):
class Document(BaseCZMLObject):
"""A CZML document, consisting on a list of packets."""

@property
def packets(self):
return self._values
packets: list[Packet | Preamble]

@model_serializer
def custom_serializer(self):
return list(self.packets)
Loading

0 comments on commit 78fcbe9

Please sign in to comment.