Créer un modèle pydantic pour les polygones SIG

Dans cet article, je vais expliquer comment créer un modèle pydantic pour valider et créer des polygones pour des SIG (systèmes d’information géographique).

On commence par créer des contraintes pour Latitude et Longitude.

from pydantic import BaseModel, ConstrainedFloat

class Latitude(ConstrainedFloat):
    ge = -90
    le = 90


class Longitude(ConstrainedFloat):
    ge = -180
    le = 180

Ces types seront pratiques. Vous pouvez, par exemple, les utiliser pour créer un modèle Coordinates.

class Coordinates(BaseModel):

    lat: Latitude
    lng: Longitude

Maintenant, le polygone. Pour cette démonstration nous allons calquer et utiliser Shapely, qui prend une série coordonnées pour définir une surface, puis aucune ou plusieurs autres séries pour définir des trous. Chaque série doit avoir au moins 3 coordonnées. On obtient donc:

from typing import List, Tuple

from pydantic import conlist

class MyModel(BaseModel):

    polygon: conlist(conlist(Tuple[Longitude, Latitude], min_items=3), min_items=1)

MyModel.parse_obj({
    "polygon": [
        # shell
        [(45, -100), (45, -101), (46, -101)],
        # hole
        [(45.3, -100.3), (45.3, -100.6), (44.6, -101.6)]
    ]
})

Maintenant, pour rendre le type réutilisable, il faut déclarer un nouveau modèle et utiliser la propriété __root__ comme suit:

from typing import Any

class Polygon(BaseModel):
    __root__: conlist(conlist(Tuple[Longitude, Latitude], min_items=3), min_items=1)

    def __init__(self, **data: Any):
        super().__init__(**data)

class MyModel(BaseModel):

    polygon: Polygon

Pour pousser encore plus loin, pourquoi ne pas initialiser l’objet Shapely.geometry.Polygon dès la création du modèle? Notez que j’ai renommé Polygon à RawPolygon pour éviter tout conflit avec shapely.

from pydantic import Field, PrivateAttr
from shapely.geometry import Polygon

class RawPolygon(BaseModel):
    __root__: conlist(conlist(Tuple[Longitude, Latitude], min_items=3), min_items=1)
    _polygon: Polygon = PrivateAttr()

    def __init__(self, **data: Any):
        super().__init__(**data)
        self._polygon = Polygon(data["__root__"][0], data["__root__"][1:])

    @property
    def polygon(self) -> Polygon:
        return self._polygon

class MyModel(BaseModel):

    raw_polygon: RawPolygon = Field(..., alias="polygon")

    @property
    def polygon(self) -> Polygon:
        return self.raw_polygon.polygon

Et notre exemple initial fonctionne encore en plus du polygone directement disponible.

m = MyModel.parse_obj({
    "polygon": [
        # polygon
        [(45, -100), (45, -101), (46, -101)],
        # hole
        [(45.3, -100.3), (45.3, -100.6), (44.6, -101.6)]
    ]
})

print(m.polygon)
#> POLYGON ((45 -100, 45 -101, 46 -101, 45 -100), (45.3 -100.3, 45.3 -100.6, 44.6 -101.6, 45.3 -100.3))

Code Complet

from typing import Any, Tuple

from pydantic import BaseModel, ConstrainedFloat, Field, PrivateAttr, conlist
from shapely.geometry import Polygon


class Latitude(ConstrainedFloat):
    ge = -90
    le = 90


class Longitude(ConstrainedFloat):
    ge = -180
    le = 180

class RawPolygon(BaseModel):
    __root__: conlist(conlist(Tuple[Longitude, Latitude], min_items=3), min_items=1)
    _polygon: Polygon = PrivateAttr()

    def __init__(self, **data: Any):
        super().__init__(**data)
        self._polygon = Polygon(data["__root__"][0], data["__root__"][1:])

    @property
    def polygon(self) -> Polygon:
        return self._polygon

class MyModel(BaseModel):

    raw_polygon: RawPolygon = Field(..., alias="polygon")

    @property
    def polygon(self) -> Polygon:
        return self.raw_polygon.polygon

Leave a Reply

Ce site utilise Akismet pour réduire le pourriel. En savoir plus sur comment les données de vos commentaires sont utilisées.