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