overturetoosm.objects

Pydantic models needed throughout the project.

  1"""Pydantic models needed throughout the project."""
  2
  3# ruff: noqa: D415
  4
  5from enum import Enum
  6
  7try:
  8    from typing import Annotated
  9except ImportError:
 10    from typing import Annotated
 11
 12from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator
 13
 14from .resources import places_tags
 15
 16
 17class OvertureBaseModel(BaseModel):
 18    """Base model for Overture features."""
 19
 20    model_config = ConfigDict(extra="forbid")
 21
 22    version: int = Field(ge=0)
 23    theme: str | None = None
 24    type: str | None = None
 25    id: str | None = Field(None, pattern=r"^(\S.*)?\S$")
 26
 27
 28class Wikidata(RootModel):
 29    """Model for transportation segment wikidata."""
 30
 31    root: str = Field(description="Wikidata ID.", pattern=r"^Q\d+")
 32
 33
 34class Sources(BaseModel):
 35    """Overture sources model."""
 36
 37    property: str
 38    dataset: str
 39    record_id: str | None = None
 40    confidence: float | None = Field(ge=0.0, le=1.0)
 41    update_time: str | None = None
 42
 43    @field_validator("confidence")
 44    @classmethod
 45    def set_default_if_none(cls, v: float) -> float:
 46        """@private"""
 47        return v if v is not None else 0.0
 48
 49    def get_osm_link(self) -> str | None:
 50        """Return the OSM link for the source."""
 51        if (
 52            self.record_id
 53            and self.record_id.startswith(("n", "w", "r"))
 54            and self.dataset == "OpenStreetMap"
 55        ):
 56            type_dict = {"n": "node", "w": "way", "r": "relation"}
 57            return f"https://www.openstreetmap.org/{type_dict[self.record_id[0]]}/{self.record_id[1:]}"
 58
 59
 60class RulesVariant(str, Enum):
 61    """Overture name rules variant model."""
 62
 63    alternate = "alternate"
 64    common = "common"
 65    official = "official"
 66    short = "short"
 67
 68
 69class Between(RootModel):
 70    """Model for transportation segment between."""
 71
 72    root: Annotated[list, Field(float, min_length=2, max_length=2)]
 73
 74
 75class Mode(str, Enum):
 76    """Model for political perspectives from which a named feature is viewed."""
 77
 78    accepted_by = "accepted_by"
 79    disputed_by = "disputed_by"
 80
 81
 82class Perspectives(BaseModel):
 83    """Model for political perspectives from which a named feature is viewed."""
 84
 85    mode: Mode
 86    countries: list[str] = Field(min_length=1)
 87
 88
 89class Rules(BaseModel):
 90    """Overture name rules model."""
 91
 92    variant: RulesVariant
 93    language: str | None = None
 94    value: str
 95    between: Between | None = None
 96    side: str | None = None
 97    perspectives: Perspectives | None = None
 98
 99
100class Names(BaseModel):
101    """Overture names model."""
102
103    primary: str
104    common: list[tuple[str, str]] | None
105    rules: list[Rules] | None
106
107    def to_osm(self) -> dict[str, str]:
108        """Convert names to OSM tags."""
109        names = {}
110        if self.primary:
111            names["name"] = self.primary
112
113        return names
114
115
116class PlaceAddress(BaseModel):
117    """Overture addresses model."""
118
119    freeform: str | None
120    locality: str | None
121    postcode: str | None
122    region: str | None
123    country: str | None = Field(pattern=r"^[A-Z]{2}$")
124
125    def to_osm(self, region_tag: str) -> dict[str, str]:
126        """Convert address to OSM tags."""
127        address_info = {}
128        if self.freeform:
129            address_info["addr:street_address"] = self.freeform
130        if self.country:
131            address_info["addr:country"] = self.country
132        if self.postcode:
133            address_info["addr:postcode"] = self.postcode
134        if self.locality:
135            address_info["addr:city"] = self.locality
136        if self.region:
137            address_info[region_tag] = self.region
138
139        return address_info
140
141
142class Categories(BaseModel):
143    """Overture categories model."""
144
145    primary: str
146    alternate: list[str] | None
147
148    def to_osm(self, unmatched: str) -> dict[str, str]:
149        """Convert categories to OSM tags."""
150        prim = places_tags.get(self.primary)
151        if prim:
152            return prim
153        elif unmatched == "force":
154            return {"type": self.primary}
155        elif unmatched == "error":
156            raise UnmatchedError(self.primary)
157        return {}
158
159
160class Brand(BaseModel):
161    """Overture brand model."""
162
163    wikidata: Wikidata | None = None
164    names: Names
165
166    def to_osm(self) -> dict[str, str]:
167        """Convert brand properties to OSM tags."""
168        osm = {"brand": self.names.primary}
169        if self.wikidata:
170            osm.update({"brand:wikidata": str(self.wikidata.root)})
171        return osm
172
173
174class Socials(RootModel):
175    """Overture socials model."""
176
177    root: list[str]
178
179    def to_osm(self) -> dict[str, str]:
180        """Convert socials properties to OSM tags."""
181        new_props = {}
182        for social in self.root:
183            if "facebook" in social:
184                new_props["contact:facebook"] = social
185            elif "twitter" in str(social):
186                new_props["contact:twitter"] = social
187        return new_props
188
189
190class PlaceProps(OvertureBaseModel):
191    """Overture properties model.
192
193    Use this model directly if you want to manipulate the `place` properties yourself.
194    """
195
196    sources: list[Sources]
197    names: Names
198    brand: Brand | None = None
199    categories: Categories | None = None
200    confidence: float = Field(ge=0.0, le=1.0)
201    websites: list[str] | None = None
202    socials: Socials | None = None
203    emails: list[str] | None = None
204    phones: list[str] | None = None
205    addresses: list[PlaceAddress]
206
207    def to_osm(
208        self, confidence: float, region_tag: str, unmatched: str
209    ) -> dict[str, str]:
210        """Convert Overture's place properties to OSM tags.
211
212        Used internally by the `overturetoosm.process_place` function.
213        """
214        if self.confidence < confidence:
215            raise ConfidenceError(confidence, self.confidence)
216
217        new_props = {}
218
219        # Categories
220        if self.categories:
221            new_props.update(self.categories.to_osm(unmatched))
222
223        # Names
224        if self.names:
225            new_props.update(self.names.to_osm())
226
227        # Contact information
228        new_props.update(self._process_contact_info())
229
230        # Addresses
231        if self.addresses:
232            new_props.update(self.addresses[0].to_osm(region_tag))
233
234        # Sources
235        new_props["source"] = source_statement(self.sources)
236
237        # Socials and Brand
238        if self.socials:
239            new_props.update(self.socials.to_osm())
240        if self.brand:
241            new_props.update(self.brand.to_osm())
242
243        return new_props
244
245    def _process_contact_info(self) -> dict[str, str]:
246        """Process contact information."""
247        contact_info = {}
248        if self.phones is not None:
249            contact_info["phone"] = self.phones[0]
250        if self.websites is not None and self.websites[0]:
251            contact_info["website"] = str(self.websites[0])
252        return contact_info
253
254
255class ConfidenceError(Exception):
256    """Confidence error exception.
257
258    This exception is raised when the confidence level of an item is below the
259    user-defined level. It contains the original confidence level and the confidence
260    level of the item.
261
262    Attributes:
263        confidence_level (float): The set confidence level.
264        confidence_item (float): The confidence of the item.
265        message (str): The error message.
266    """
267
268    def __init__(
269        self,
270        confidence_level: float,
271        confidence_item: float,
272        message: str = "Confidence in this item is too low.",
273    ) -> None:
274        """@private"""
275        self.confidence_level = confidence_level
276        self.confidence_item = confidence_item
277        self.message = message
278        super().__init__(message)
279
280    def __str__(self) -> str:
281        """@private"""
282        lev = f"confidence_level={self.confidence_level}"
283        item = f"confidence_item={self.confidence_item}"
284        return f"""{self.message} {lev}, {item}"""
285
286
287class UnmatchedError(Exception):
288    """Unmatched category error.
289
290    This exception is raised when an item's Overture category does not have a
291    corresponding OSM definition. Edit
292    [the OSM Wiki page](https://wiki.openstreetmap.org/wiki/Overture_categories)
293    to add a definition to this category.
294
295    Attributes:
296        category (str): The Overture category that is unmatched.
297        message (str): The error message.
298    """
299
300    def __init__(
301        self, category: str, message: str = "Overture category is unmatched."
302    ) -> None:
303        """@private"""
304        self.category = category
305        self.message = message
306        super().__init__(message)
307
308    def __str__(self) -> str:
309        """@private"""
310        return f"{self.message} {{category={self.category}}}"
311
312
313class BuildingProps(OvertureBaseModel):
314    """Overture building properties.
315
316    Use this model if you want to manipulate the `building` properties yourself.
317    """
318
319    has_parts: bool
320    sources: list[Sources]
321    class_: str | None = Field(alias="class", default=None)
322    subtype: str | None = None
323    names: Names | None = None
324    level: int | None = None
325    height: float | None = None
326    is_underground: bool | None = None
327    num_floors: int | None = Field(serialization_alias="building:levels", default=None)
328    num_floors_underground: int | None = Field(
329        serialization_alias="building:levels:underground", default=None
330    )
331    min_height: float | None = None
332    min_floor: int | None = Field(
333        serialization_alias="building:min_level", default=None
334    )
335    facade_color: str | None = Field(
336        serialization_alias="building:colour", default=None
337    )
338    facade_material: str | None = Field(
339        serialization_alias="building:material", default=None
340    )
341    roof_material: str | None = Field(serialization_alias="roof:material", default=None)
342    roof_shape: str | None = Field(serialization_alias="roof:shape", default=None)
343    roof_direction: str | None = Field(
344        serialization_alias="roof:direction", default=None
345    )
346    roof_orientation: str | None = Field(
347        serialization_alias="roof:orientation", default=None
348    )
349    roof_color: str | None = Field(serialization_alias="roof:colour", default=None)
350    roof_height: float | None = Field(serialization_alias="roof:height", default=None)
351
352    def to_osm(self, confidence: float) -> dict[str, str]:
353        """Convert properties to OSM tags.
354
355        Used internally by`overturetoosm.process_building` function.
356        """
357        new_props = {}
358        confidences = {source.confidence for source in self.sources}
359        if any(conf and conf < confidence for conf in confidences):
360            raise ConfidenceError(confidence, max({i for i in confidences if i}))
361
362        new_props["building"] = self.class_ if self.class_ else "yes"
363
364        new_props["source"] = source_statement(self.sources)
365
366        prop_obj = self.model_dump(exclude_none=True, by_alias=True).items()
367        new_props.update(
368            {k: v for k, v in prop_obj if k.startswith(("roof", "building"))}
369        )
370        new_props.update({k: round(v, 2) for k, v in prop_obj if k.endswith("height")})
371
372        if self.is_underground:
373            new_props["location"] = "underground"
374        if self.names:
375            new_props["name"] = self.names.primary
376        return new_props
377
378
379class AddressLevel(BaseModel):
380    """Overture address level model."""
381
382    value: str
383
384
385class AddressProps(OvertureBaseModel):
386    """Overture address properties.
387
388    Use this model directly if you want to manipulate the `address` properties yourself.
389    """
390
391    number: str | None = Field(serialization_alias="addr:housenumber", default=None)
392    street: str | None = Field(serialization_alias="addr:street", default=None)
393    unit: str | None = Field(serialization_alias="addr:unit", default=None)
394    postcode: str | None = Field(serialization_alias="addr:postcode", default=None)
395    postal_city: str | None = Field(serialization_alias="addr:city", default=None)
396    country: str | None = Field(serialization_alias="addr:country", default=None)
397    address_levels: (
398        None | (Annotated[list[AddressLevel], Field(min_length=1, max_length=5)])
399    ) = Field(default_factory=list)
400    sources: list[Sources]
401
402    def to_osm(self, style: str) -> dict[str, str]:
403        """Convert properties to OSM tags.
404
405        Used internally by `overturetoosm.process_address`.
406        """
407        obj_dict = {
408            k: v
409            for k, v in self.model_dump(exclude_none=True, by_alias=True).items()
410            if k.startswith("addr:")
411        }
412        obj_dict["source"] = source_statement(self.sources)
413
414        if self.address_levels and len(self.address_levels) > 0 and style == "US":
415            obj_dict["addr:state"] = str(self.address_levels[0].value)
416
417        return obj_dict
418
419
420def source_statement(source: list[Sources]) -> str:
421    """Return a source statement from a list of sources."""
422    return (
423        ", ".join(sorted({i.dataset.strip(", ") for i in source}))
424        + " via overturetoosm"
425    )
class OvertureBaseModel(pydantic.main.BaseModel):
18class OvertureBaseModel(BaseModel):
19    """Base model for Overture features."""
20
21    model_config = ConfigDict(extra="forbid")
22
23    version: int = Field(ge=0)
24    theme: str | None = None
25    type: str | None = None
26    id: str | None = Field(None, pattern=r"^(\S.*)?\S$")

Base model for Overture features.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
theme: str | None
type: str | None
id: str | None
class Wikidata(pydantic.main.BaseModel, typing.Generic[~RootModelRootType]):
29class Wikidata(RootModel):
30    """Model for transportation segment wikidata."""
31
32    root: str = Field(description="Wikidata ID.", pattern=r"^Q\d+")

Model for transportation segment wikidata.

root: str
class Sources(pydantic.main.BaseModel):
35class Sources(BaseModel):
36    """Overture sources model."""
37
38    property: str
39    dataset: str
40    record_id: str | None = None
41    confidence: float | None = Field(ge=0.0, le=1.0)
42    update_time: str | None = None
43
44    @field_validator("confidence")
45    @classmethod
46    def set_default_if_none(cls, v: float) -> float:
47        """@private"""
48        return v if v is not None else 0.0
49
50    def get_osm_link(self) -> str | None:
51        """Return the OSM link for the source."""
52        if (
53            self.record_id
54            and self.record_id.startswith(("n", "w", "r"))
55            and self.dataset == "OpenStreetMap"
56        ):
57            type_dict = {"n": "node", "w": "way", "r": "relation"}
58            return f"https://www.openstreetmap.org/{type_dict[self.record_id[0]]}/{self.record_id[1:]}"

Overture sources model.

property: str
dataset: str
record_id: str | None
confidence: float | None
update_time: str | None
model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class RulesVariant(builtins.str, enum.Enum):
61class RulesVariant(str, Enum):
62    """Overture name rules variant model."""
63
64    alternate = "alternate"
65    common = "common"
66    official = "official"
67    short = "short"

Overture name rules variant model.

alternate = <RulesVariant.alternate: 'alternate'>
common = <RulesVariant.common: 'common'>
official = <RulesVariant.official: 'official'>
short = <RulesVariant.short: 'short'>
class Between(pydantic.main.BaseModel, typing.Generic[~RootModelRootType]):
70class Between(RootModel):
71    """Model for transportation segment between."""
72
73    root: Annotated[list, Field(float, min_length=2, max_length=2)]

Model for transportation segment between.

root: Annotated[list, FieldInfo(annotation=NoneType, required=False, default=<class 'float'>, metadata=[MinLen(min_length=2), MaxLen(max_length=2)])]
class Mode(builtins.str, enum.Enum):
76class Mode(str, Enum):
77    """Model for political perspectives from which a named feature is viewed."""
78
79    accepted_by = "accepted_by"
80    disputed_by = "disputed_by"

Model for political perspectives from which a named feature is viewed.

accepted_by = <Mode.accepted_by: 'accepted_by'>
disputed_by = <Mode.disputed_by: 'disputed_by'>
class Perspectives(pydantic.main.BaseModel):
83class Perspectives(BaseModel):
84    """Model for political perspectives from which a named feature is viewed."""
85
86    mode: Mode
87    countries: list[str] = Field(min_length=1)

Model for political perspectives from which a named feature is viewed.

mode: Mode
countries: list[str]
model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class Rules(pydantic.main.BaseModel):
90class Rules(BaseModel):
91    """Overture name rules model."""
92
93    variant: RulesVariant
94    language: str | None = None
95    value: str
96    between: Between | None = None
97    side: str | None = None
98    perspectives: Perspectives | None = None

Overture name rules model.

variant: RulesVariant
language: str | None
value: str
between: Between | None
side: str | None
perspectives: Perspectives | None
model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class Names(pydantic.main.BaseModel):
101class Names(BaseModel):
102    """Overture names model."""
103
104    primary: str
105    common: list[tuple[str, str]] | None
106    rules: list[Rules] | None
107
108    def to_osm(self) -> dict[str, str]:
109        """Convert names to OSM tags."""
110        names = {}
111        if self.primary:
112            names["name"] = self.primary
113
114        return names

Overture names model.

primary: str
common: list[tuple[str, str]] | None
rules: list[Rules] | None
def to_osm(self) -> dict[str, str]:
108    def to_osm(self) -> dict[str, str]:
109        """Convert names to OSM tags."""
110        names = {}
111        if self.primary:
112            names["name"] = self.primary
113
114        return names

Convert names to OSM tags.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class PlaceAddress(pydantic.main.BaseModel):
117class PlaceAddress(BaseModel):
118    """Overture addresses model."""
119
120    freeform: str | None
121    locality: str | None
122    postcode: str | None
123    region: str | None
124    country: str | None = Field(pattern=r"^[A-Z]{2}$")
125
126    def to_osm(self, region_tag: str) -> dict[str, str]:
127        """Convert address to OSM tags."""
128        address_info = {}
129        if self.freeform:
130            address_info["addr:street_address"] = self.freeform
131        if self.country:
132            address_info["addr:country"] = self.country
133        if self.postcode:
134            address_info["addr:postcode"] = self.postcode
135        if self.locality:
136            address_info["addr:city"] = self.locality
137        if self.region:
138            address_info[region_tag] = self.region
139
140        return address_info

Overture addresses model.

freeform: str | None
locality: str | None
postcode: str | None
region: str | None
country: str | None
def to_osm(self, region_tag: str) -> dict[str, str]:
126    def to_osm(self, region_tag: str) -> dict[str, str]:
127        """Convert address to OSM tags."""
128        address_info = {}
129        if self.freeform:
130            address_info["addr:street_address"] = self.freeform
131        if self.country:
132            address_info["addr:country"] = self.country
133        if self.postcode:
134            address_info["addr:postcode"] = self.postcode
135        if self.locality:
136            address_info["addr:city"] = self.locality
137        if self.region:
138            address_info[region_tag] = self.region
139
140        return address_info

Convert address to OSM tags.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class Categories(pydantic.main.BaseModel):
143class Categories(BaseModel):
144    """Overture categories model."""
145
146    primary: str
147    alternate: list[str] | None
148
149    def to_osm(self, unmatched: str) -> dict[str, str]:
150        """Convert categories to OSM tags."""
151        prim = places_tags.get(self.primary)
152        if prim:
153            return prim
154        elif unmatched == "force":
155            return {"type": self.primary}
156        elif unmatched == "error":
157            raise UnmatchedError(self.primary)
158        return {}

Overture categories model.

primary: str
alternate: list[str] | None
def to_osm(self, unmatched: str) -> dict[str, str]:
149    def to_osm(self, unmatched: str) -> dict[str, str]:
150        """Convert categories to OSM tags."""
151        prim = places_tags.get(self.primary)
152        if prim:
153            return prim
154        elif unmatched == "force":
155            return {"type": self.primary}
156        elif unmatched == "error":
157            raise UnmatchedError(self.primary)
158        return {}

Convert categories to OSM tags.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class Brand(pydantic.main.BaseModel):
161class Brand(BaseModel):
162    """Overture brand model."""
163
164    wikidata: Wikidata | None = None
165    names: Names
166
167    def to_osm(self) -> dict[str, str]:
168        """Convert brand properties to OSM tags."""
169        osm = {"brand": self.names.primary}
170        if self.wikidata:
171            osm.update({"brand:wikidata": str(self.wikidata.root)})
172        return osm

Overture brand model.

wikidata: Wikidata | None
names: Names
def to_osm(self) -> dict[str, str]:
167    def to_osm(self) -> dict[str, str]:
168        """Convert brand properties to OSM tags."""
169        osm = {"brand": self.names.primary}
170        if self.wikidata:
171            osm.update({"brand:wikidata": str(self.wikidata.root)})
172        return osm

Convert brand properties to OSM tags.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class Socials(pydantic.main.BaseModel, typing.Generic[~RootModelRootType]):
175class Socials(RootModel):
176    """Overture socials model."""
177
178    root: list[str]
179
180    def to_osm(self) -> dict[str, str]:
181        """Convert socials properties to OSM tags."""
182        new_props = {}
183        for social in self.root:
184            if "facebook" in social:
185                new_props["contact:facebook"] = social
186            elif "twitter" in str(social):
187                new_props["contact:twitter"] = social
188        return new_props

Overture socials model.

root: list[str]
def to_osm(self) -> dict[str, str]:
180    def to_osm(self) -> dict[str, str]:
181        """Convert socials properties to OSM tags."""
182        new_props = {}
183        for social in self.root:
184            if "facebook" in social:
185                new_props["contact:facebook"] = social
186            elif "twitter" in str(social):
187                new_props["contact:twitter"] = social
188        return new_props

Convert socials properties to OSM tags.

class PlaceProps(OvertureBaseModel):
191class PlaceProps(OvertureBaseModel):
192    """Overture properties model.
193
194    Use this model directly if you want to manipulate the `place` properties yourself.
195    """
196
197    sources: list[Sources]
198    names: Names
199    brand: Brand | None = None
200    categories: Categories | None = None
201    confidence: float = Field(ge=0.0, le=1.0)
202    websites: list[str] | None = None
203    socials: Socials | None = None
204    emails: list[str] | None = None
205    phones: list[str] | None = None
206    addresses: list[PlaceAddress]
207
208    def to_osm(
209        self, confidence: float, region_tag: str, unmatched: str
210    ) -> dict[str, str]:
211        """Convert Overture's place properties to OSM tags.
212
213        Used internally by the `overturetoosm.process_place` function.
214        """
215        if self.confidence < confidence:
216            raise ConfidenceError(confidence, self.confidence)
217
218        new_props = {}
219
220        # Categories
221        if self.categories:
222            new_props.update(self.categories.to_osm(unmatched))
223
224        # Names
225        if self.names:
226            new_props.update(self.names.to_osm())
227
228        # Contact information
229        new_props.update(self._process_contact_info())
230
231        # Addresses
232        if self.addresses:
233            new_props.update(self.addresses[0].to_osm(region_tag))
234
235        # Sources
236        new_props["source"] = source_statement(self.sources)
237
238        # Socials and Brand
239        if self.socials:
240            new_props.update(self.socials.to_osm())
241        if self.brand:
242            new_props.update(self.brand.to_osm())
243
244        return new_props
245
246    def _process_contact_info(self) -> dict[str, str]:
247        """Process contact information."""
248        contact_info = {}
249        if self.phones is not None:
250            contact_info["phone"] = self.phones[0]
251        if self.websites is not None and self.websites[0]:
252            contact_info["website"] = str(self.websites[0])
253        return contact_info

Overture properties model.

Use this model directly if you want to manipulate the place properties yourself.

sources: list[Sources]
names: Names
brand: Brand | None
categories: Categories | None
confidence: float
websites: list[str] | None
socials: Socials | None
emails: list[str] | None
phones: list[str] | None
addresses: list[PlaceAddress]
def to_osm( self, confidence: float, region_tag: str, unmatched: str) -> dict[str, str]:
208    def to_osm(
209        self, confidence: float, region_tag: str, unmatched: str
210    ) -> dict[str, str]:
211        """Convert Overture's place properties to OSM tags.
212
213        Used internally by the `overturetoosm.process_place` function.
214        """
215        if self.confidence < confidence:
216            raise ConfidenceError(confidence, self.confidence)
217
218        new_props = {}
219
220        # Categories
221        if self.categories:
222            new_props.update(self.categories.to_osm(unmatched))
223
224        # Names
225        if self.names:
226            new_props.update(self.names.to_osm())
227
228        # Contact information
229        new_props.update(self._process_contact_info())
230
231        # Addresses
232        if self.addresses:
233            new_props.update(self.addresses[0].to_osm(region_tag))
234
235        # Sources
236        new_props["source"] = source_statement(self.sources)
237
238        # Socials and Brand
239        if self.socials:
240            new_props.update(self.socials.to_osm())
241        if self.brand:
242            new_props.update(self.brand.to_osm())
243
244        return new_props

Convert Overture's place properties to OSM tags.

Used internally by the overturetoosm.process_place function.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
OvertureBaseModel
version
theme
type
id
class ConfidenceError(builtins.Exception):
256class ConfidenceError(Exception):
257    """Confidence error exception.
258
259    This exception is raised when the confidence level of an item is below the
260    user-defined level. It contains the original confidence level and the confidence
261    level of the item.
262
263    Attributes:
264        confidence_level (float): The set confidence level.
265        confidence_item (float): The confidence of the item.
266        message (str): The error message.
267    """
268
269    def __init__(
270        self,
271        confidence_level: float,
272        confidence_item: float,
273        message: str = "Confidence in this item is too low.",
274    ) -> None:
275        """@private"""
276        self.confidence_level = confidence_level
277        self.confidence_item = confidence_item
278        self.message = message
279        super().__init__(message)
280
281    def __str__(self) -> str:
282        """@private"""
283        lev = f"confidence_level={self.confidence_level}"
284        item = f"confidence_item={self.confidence_item}"
285        return f"""{self.message} {lev}, {item}"""

Confidence error exception.

This exception is raised when the confidence level of an item is below the user-defined level. It contains the original confidence level and the confidence level of the item.

Attributes:
  • confidence_level (float): The set confidence level.
  • confidence_item (float): The confidence of the item.
  • message (str): The error message.
confidence_level
confidence_item
message
class UnmatchedError(builtins.Exception):
288class UnmatchedError(Exception):
289    """Unmatched category error.
290
291    This exception is raised when an item's Overture category does not have a
292    corresponding OSM definition. Edit
293    [the OSM Wiki page](https://wiki.openstreetmap.org/wiki/Overture_categories)
294    to add a definition to this category.
295
296    Attributes:
297        category (str): The Overture category that is unmatched.
298        message (str): The error message.
299    """
300
301    def __init__(
302        self, category: str, message: str = "Overture category is unmatched."
303    ) -> None:
304        """@private"""
305        self.category = category
306        self.message = message
307        super().__init__(message)
308
309    def __str__(self) -> str:
310        """@private"""
311        return f"{self.message} {{category={self.category}}}"

Unmatched category error.

This exception is raised when an item's Overture category does not have a corresponding OSM definition. Edit the OSM Wiki page to add a definition to this category.

Attributes:
  • category (str): The Overture category that is unmatched.
  • message (str): The error message.
category
message
class BuildingProps(OvertureBaseModel):
314class BuildingProps(OvertureBaseModel):
315    """Overture building properties.
316
317    Use this model if you want to manipulate the `building` properties yourself.
318    """
319
320    has_parts: bool
321    sources: list[Sources]
322    class_: str | None = Field(alias="class", default=None)
323    subtype: str | None = None
324    names: Names | None = None
325    level: int | None = None
326    height: float | None = None
327    is_underground: bool | None = None
328    num_floors: int | None = Field(serialization_alias="building:levels", default=None)
329    num_floors_underground: int | None = Field(
330        serialization_alias="building:levels:underground", default=None
331    )
332    min_height: float | None = None
333    min_floor: int | None = Field(
334        serialization_alias="building:min_level", default=None
335    )
336    facade_color: str | None = Field(
337        serialization_alias="building:colour", default=None
338    )
339    facade_material: str | None = Field(
340        serialization_alias="building:material", default=None
341    )
342    roof_material: str | None = Field(serialization_alias="roof:material", default=None)
343    roof_shape: str | None = Field(serialization_alias="roof:shape", default=None)
344    roof_direction: str | None = Field(
345        serialization_alias="roof:direction", default=None
346    )
347    roof_orientation: str | None = Field(
348        serialization_alias="roof:orientation", default=None
349    )
350    roof_color: str | None = Field(serialization_alias="roof:colour", default=None)
351    roof_height: float | None = Field(serialization_alias="roof:height", default=None)
352
353    def to_osm(self, confidence: float) -> dict[str, str]:
354        """Convert properties to OSM tags.
355
356        Used internally by`overturetoosm.process_building` function.
357        """
358        new_props = {}
359        confidences = {source.confidence for source in self.sources}
360        if any(conf and conf < confidence for conf in confidences):
361            raise ConfidenceError(confidence, max({i for i in confidences if i}))
362
363        new_props["building"] = self.class_ if self.class_ else "yes"
364
365        new_props["source"] = source_statement(self.sources)
366
367        prop_obj = self.model_dump(exclude_none=True, by_alias=True).items()
368        new_props.update(
369            {k: v for k, v in prop_obj if k.startswith(("roof", "building"))}
370        )
371        new_props.update({k: round(v, 2) for k, v in prop_obj if k.endswith("height")})
372
373        if self.is_underground:
374            new_props["location"] = "underground"
375        if self.names:
376            new_props["name"] = self.names.primary
377        return new_props

Overture building properties.

Use this model if you want to manipulate the building properties yourself.

has_parts: bool
sources: list[Sources]
class_: str | None
subtype: str | None
names: Names | None
level: int | None
height: float | None
is_underground: bool | None
num_floors: int | None
num_floors_underground: int | None
min_height: float | None
min_floor: int | None
facade_color: str | None
facade_material: str | None
roof_material: str | None
roof_shape: str | None
roof_direction: str | None
roof_orientation: str | None
roof_color: str | None
roof_height: float | None
def to_osm(self, confidence: float) -> dict[str, str]:
353    def to_osm(self, confidence: float) -> dict[str, str]:
354        """Convert properties to OSM tags.
355
356        Used internally by`overturetoosm.process_building` function.
357        """
358        new_props = {}
359        confidences = {source.confidence for source in self.sources}
360        if any(conf and conf < confidence for conf in confidences):
361            raise ConfidenceError(confidence, max({i for i in confidences if i}))
362
363        new_props["building"] = self.class_ if self.class_ else "yes"
364
365        new_props["source"] = source_statement(self.sources)
366
367        prop_obj = self.model_dump(exclude_none=True, by_alias=True).items()
368        new_props.update(
369            {k: v for k, v in prop_obj if k.startswith(("roof", "building"))}
370        )
371        new_props.update({k: round(v, 2) for k, v in prop_obj if k.endswith("height")})
372
373        if self.is_underground:
374            new_props["location"] = "underground"
375        if self.names:
376            new_props["name"] = self.names.primary
377        return new_props

Convert properties to OSM tags.

Used internally byoverturetoosm.process_building function.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
OvertureBaseModel
version
theme
type
id
class AddressLevel(pydantic.main.BaseModel):
380class AddressLevel(BaseModel):
381    """Overture address level model."""
382
383    value: str

Overture address level model.

value: str
model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class AddressProps(OvertureBaseModel):
386class AddressProps(OvertureBaseModel):
387    """Overture address properties.
388
389    Use this model directly if you want to manipulate the `address` properties yourself.
390    """
391
392    number: str | None = Field(serialization_alias="addr:housenumber", default=None)
393    street: str | None = Field(serialization_alias="addr:street", default=None)
394    unit: str | None = Field(serialization_alias="addr:unit", default=None)
395    postcode: str | None = Field(serialization_alias="addr:postcode", default=None)
396    postal_city: str | None = Field(serialization_alias="addr:city", default=None)
397    country: str | None = Field(serialization_alias="addr:country", default=None)
398    address_levels: (
399        None | (Annotated[list[AddressLevel], Field(min_length=1, max_length=5)])
400    ) = Field(default_factory=list)
401    sources: list[Sources]
402
403    def to_osm(self, style: str) -> dict[str, str]:
404        """Convert properties to OSM tags.
405
406        Used internally by `overturetoosm.process_address`.
407        """
408        obj_dict = {
409            k: v
410            for k, v in self.model_dump(exclude_none=True, by_alias=True).items()
411            if k.startswith("addr:")
412        }
413        obj_dict["source"] = source_statement(self.sources)
414
415        if self.address_levels and len(self.address_levels) > 0 and style == "US":
416            obj_dict["addr:state"] = str(self.address_levels[0].value)
417
418        return obj_dict

Overture address properties.

Use this model directly if you want to manipulate the address properties yourself.

number: str | None
street: str | None
unit: str | None
postcode: str | None
postal_city: str | None
country: str | None
address_levels: Optional[Annotated[list[AddressLevel], FieldInfo(annotation=NoneType, required=True, metadata=[MinLen(min_length=1), MaxLen(max_length=5)])]]
sources: list[Sources]
def to_osm(self, style: str) -> dict[str, str]:
403    def to_osm(self, style: str) -> dict[str, str]:
404        """Convert properties to OSM tags.
405
406        Used internally by `overturetoosm.process_address`.
407        """
408        obj_dict = {
409            k: v
410            for k, v in self.model_dump(exclude_none=True, by_alias=True).items()
411            if k.startswith("addr:")
412        }
413        obj_dict["source"] = source_statement(self.sources)
414
415        if self.address_levels and len(self.address_levels) > 0 and style == "US":
416            obj_dict["addr:state"] = str(self.address_levels[0].value)
417
418        return obj_dict

Convert properties to OSM tags.

Used internally by overturetoosm.process_address.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
OvertureBaseModel
version
theme
type
id
def source_statement(source: list[Sources]) -> str:
421def source_statement(source: list[Sources]) -> str:
422    """Return a source statement from a list of sources."""
423    return (
424        ", ".join(sorted({i.dataset.strip(", ") for i in source}))
425        + " via overturetoosm"
426    )

Return a source statement from a list of sources.