File: base.py

package info (click to toggle)
geojson-pydantic 2.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 216 kB
  • sloc: python: 1,503; makefile: 3
file content (83 lines) | stat: -rw-r--r-- 3,062 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"""pydantic BaseModel for GeoJSON objects."""

from __future__ import annotations

import warnings
from typing import Any, Dict, List, Optional, Set

from pydantic import BaseModel, SerializationInfo, field_validator, model_serializer

from geojson_pydantic.types import BBox


class _GeoJsonBase(BaseModel):
    bbox: Optional[BBox] = None

    # These fields will not be included when serializing in json mode
    # `.model_dump_json()` or `.model_dump(mode="json")`
    __geojson_exclude_if_none__: Set[str] = {"bbox"}

    @property
    def __geo_interface__(self) -> Dict[str, Any]:
        """GeoJSON-like protocol for geo-spatial (GIS) vector data.

        ref: https://gist.github.com/sgillies/2217756#__geo_interface
        """
        return self.model_dump(mode="json")

    @field_validator("bbox")
    def validate_bbox(cls, bbox: Optional[BBox]) -> Optional[BBox]:
        """Validate BBox values are ordered correctly."""
        # If bbox is None, there is nothing to validate.
        if bbox is None:
            return None

        # A list to store any errors found so we can raise them all at once.
        errors: List[str] = []

        # Determine where the second position starts. 2 for 2D, 3 for 3D.
        offset = len(bbox) // 2

        # Check X
        if bbox[0] > bbox[offset]:
            warnings.warn(
                f"BBOX crossing the Antimeridian line, Min X ({bbox[0]}) > Max X ({bbox[offset]}).",
                UserWarning,
                stacklevel=1,
            )

        # Check Y
        if bbox[1] > bbox[1 + offset]:
            errors.append(f"Min Y ({bbox[1]}) must be <= Max Y ({bbox[1 + offset]}).")

        # If 3D, check Z values.
        if offset > 2 and bbox[2] > bbox[2 + offset]:
            errors.append(f"Min Z ({bbox[2]}) must be <= Max Z ({bbox[2 + offset]}).")

        # Raise any errors found.
        if errors:
            raise ValueError("Invalid BBox. Error(s): " + " ".join(errors))

        return bbox

    # This return is untyped due to a workaround until this issue is resolved:
    # https://github.com/tiangolo/fastapi/discussions/10661
    @model_serializer(when_used="always", mode="wrap")
    def clean_model(self, serializer: Any, info: SerializationInfo):  # type: ignore [no-untyped-def]
        """Custom Model serializer to match the GeoJSON specification.

        Used to remove fields which are optional but cannot be null values.
        """
        # This seems like the best way to have the least amount of unexpected consequences.
        # We want to avoid forcing values in `exclude_none` or `exclude_unset` which could
        # cause issues or unexpected behavior for downstream users.
        # ref: https://github.com/pydantic/pydantic/issues/6575
        data: Dict[str, Any] = serializer(self)

        # Only remove fields when in JSON mode.
        if info.mode_is_json():
            for field in self.__geojson_exclude_if_none__:
                if field in data and data[field] is None:
                    del data[field]

        return data