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:full"] = 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    model_config = ConfigDict(extra="ignore")
197
198    sources: list[Sources]
199    names: Names
200    brand: Brand | None = None
201    categories: Categories | None = None
202    confidence: float = Field(ge=0.0, le=1.0)
203    websites: list[str | None] | None = None
204    socials: Socials | None = None
205    emails: list[str | None] | None = None
206    phones: list[str | None] | None = None
207    addresses: list[PlaceAddress]
208
209    def to_osm(
210        self, confidence: float, region_tag: str, unmatched: str
211    ) -> dict[str, str]:
212        """Convert Overture's place properties to OSM tags.
213
214        Used internally by the `overturetoosm.process_place` function.
215        """
216        if self.confidence < confidence:
217            raise ConfidenceError(confidence, self.confidence)
218
219        new_props = {}
220
221        # Categories
222        if self.categories:
223            new_props.update(self.categories.to_osm(unmatched))
224
225        # Names
226        if self.names:
227            new_props.update(self.names.to_osm())
228
229        # Contact information
230        new_props.update(self._process_contact_info())
231
232        # Addresses
233        if self.addresses:
234            new_props.update(self.addresses[0].to_osm(region_tag))
235
236        # Sources
237        new_props["source"] = source_statement(self.sources)
238
239        # Socials and Brand
240        if self.socials:
241            new_props.update(self.socials.to_osm())
242        if self.brand:
243            new_props.update(self.brand.to_osm())
244
245        return new_props
246
247    def _process_contact_info(self) -> dict[str, str]:
248        """Process contact information."""
249        contact_info = {}
250        if not is_none_or_list_of_nones(self.phones):
251            contact_info["phone"] = self.phones[0]
252        if not is_none_or_list_of_nones(self.websites):
253            contact_info["website"] = str(self.websites[0])
254        if not is_none_or_list_of_nones(self.emails):
255            contact_info["email"] = self.emails[0]
256        return contact_info
257
258
259class ConfidenceError(Exception):
260    """Confidence error exception.
261
262    This exception is raised when the confidence level of an item is below the
263    user-defined level. It contains the original confidence level and the confidence
264    level of the item.
265
266    Attributes:
267        confidence_level (float): The set confidence level.
268        confidence_item (float): The confidence of the item.
269        message (str): The error message.
270    """
271
272    def __init__(
273        self,
274        confidence_level: float,
275        confidence_item: float,
276        message: str = "Confidence in this item is too low.",
277    ) -> None:
278        """@private"""
279        self.confidence_level = confidence_level
280        self.confidence_item = confidence_item
281        self.message = message
282        super().__init__(message)
283
284    def __str__(self) -> str:
285        """@private"""
286        lev = f"confidence_level={self.confidence_level}"
287        item = f"confidence_item={self.confidence_item}"
288        return f"""{self.message} {lev}, {item}"""
289
290
291class UnmatchedError(Exception):
292    """Unmatched category error.
293
294    This exception is raised when an item's Overture category does not have a
295    corresponding OSM definition. Edit
296    [the OSM Wiki page](https://wiki.openstreetmap.org/wiki/Overture_categories)
297    to add a definition to this category.
298
299    Attributes:
300        category (str): The Overture category that is unmatched.
301        message (str): The error message.
302    """
303
304    def __init__(
305        self, category: str, message: str = "Overture category is unmatched."
306    ) -> None:
307        """@private"""
308        self.category = category
309        self.message = message
310        super().__init__(message)
311
312    def __str__(self) -> str:
313        """@private"""
314        return f"{self.message} {{category={self.category}}}"
315
316
317class BuildingProps(OvertureBaseModel):
318    """Overture building properties.
319
320    Use this model if you want to manipulate the `building` properties yourself.
321    """
322
323    has_parts: bool
324    sources: list[Sources]
325    class_: str | None = Field(alias="class", default=None)
326    subtype: str | None = None
327    names: Names | None = None
328    level: int | None = None
329    height: float | None = None
330    is_underground: bool | None = None
331    num_floors: int | None = Field(serialization_alias="building:levels", default=None)
332    num_floors_underground: int | None = Field(
333        serialization_alias="building:levels:underground", default=None
334    )
335    min_height: float | None = None
336    min_floor: int | None = Field(
337        serialization_alias="building:min_level", default=None
338    )
339    facade_color: str | None = Field(
340        serialization_alias="building:colour", default=None
341    )
342    facade_material: str | None = Field(
343        serialization_alias="building:material", default=None
344    )
345    roof_material: str | None = Field(serialization_alias="roof:material", default=None)
346    roof_shape: str | None = Field(serialization_alias="roof:shape", default=None)
347    roof_direction: str | None = Field(
348        serialization_alias="roof:direction", default=None
349    )
350    roof_orientation: str | None = Field(
351        serialization_alias="roof:orientation", default=None
352    )
353    roof_color: str | None = Field(serialization_alias="roof:colour", default=None)
354    roof_height: float | None = Field(serialization_alias="roof:height", default=None)
355
356    def to_osm(self, confidence: float) -> dict[str, str]:
357        """Convert properties to OSM tags.
358
359        Used internally by`overturetoosm.process_building` function.
360        """
361        new_props = {}
362        confidences = {source.confidence for source in self.sources}
363        if any(conf and conf < confidence for conf in confidences):
364            raise ConfidenceError(confidence, max({i for i in confidences if i}))
365
366        new_props["building"] = self.class_ if self.class_ else "yes"
367
368        new_props["source"] = source_statement(self.sources)
369
370        prop_obj = self.model_dump(exclude_none=True, by_alias=True).items()
371        new_props.update(
372            {k: v for k, v in prop_obj if k.startswith(("roof", "building"))}
373        )
374        new_props.update({k: round(v, 2) for k, v in prop_obj if k.endswith("height")})
375
376        if self.is_underground:
377            new_props["location"] = "underground"
378        if self.names:
379            new_props["name"] = self.names.primary
380        return new_props
381
382
383class AddressLevel(BaseModel):
384    """Overture address level model."""
385
386    value: str
387
388
389class AddressProps(OvertureBaseModel):
390    """Overture address properties.
391
392    Use this model directly if you want to manipulate the `address` properties yourself.
393    """
394
395    number: str | None = Field(serialization_alias="addr:housenumber", default=None)
396    street: str | None = Field(serialization_alias="addr:street", default=None)
397    unit: str | None = Field(serialization_alias="addr:unit", default=None)
398    postcode: str | None = Field(serialization_alias="addr:postcode", default=None)
399    postal_city: str | None = Field(serialization_alias="addr:city", default=None)
400    country: str | None = Field(serialization_alias="addr:country", default=None)
401    address_levels: (
402        None | (Annotated[list[AddressLevel], Field(min_length=1, max_length=5)])
403    ) = Field(default_factory=list)
404    sources: list[Sources]
405
406    def to_osm(self, style: str) -> dict[str, str]:
407        """Convert properties to OSM tags.
408
409        Used internally by `overturetoosm.process_address`.
410        """
411        obj_dict = {
412            k: v
413            for k, v in self.model_dump(exclude_none=True, by_alias=True).items()
414            if k.startswith("addr:")
415        }
416        obj_dict["source"] = source_statement(self.sources)
417
418        if self.address_levels and len(self.address_levels) > 0 and style == "US":
419            obj_dict["addr:state"] = str(self.address_levels[0].value)
420
421        return obj_dict
422
423
424def source_statement(source: list[Sources]) -> str:
425    """Return a source statement from a list of sources."""
426    return (
427        ", ".join(sorted({i.dataset.strip(", ") for i in source}))
428        + " via overturetoosm"
429    )
430
431
432def is_none_or_list_of_nones(value) -> bool:
433    """Check whether a given value is either None or a list containing only None values.
434
435    Args:
436        value: The value to check. Can be of any type.
437
438    Returns:
439        bool: True if the value is None or a list containing only None values,
440              False otherwise.
441    """
442    if value is None:
443        return True
444
445    if isinstance(value, list):
446        return len(value) > 0 and all(item is None for item in value)
447
448    return False
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.

version: int = PydanticUndefined
theme: str | None = None
type: str | None = None
id: str | None = 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 = PydanticUndefined

Wikidata ID.

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 = PydanticUndefined
dataset: str = PydanticUndefined
record_id: str | None = None
confidence: float | None = PydanticUndefined
update_time: str | None = None
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 'float'>
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 = PydanticUndefined
countries: list[str] = PydanticUndefined
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 = PydanticUndefined
language: str | None = None
value: str = PydanticUndefined
between: Between | None = None
side: str | None = None
perspectives: Perspectives | None = None
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 = PydanticUndefined
common: list[tuple[str, str]] | None = PydanticUndefined
rules: list[Rules] | None = PydanticUndefined
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.

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:full"] = 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 = PydanticUndefined
locality: str | None = PydanticUndefined
postcode: str | None = PydanticUndefined
region: str | None = PydanticUndefined
country: str | None = PydanticUndefined
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:full"] = 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.

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 = PydanticUndefined
alternate: list[str] | None = PydanticUndefined
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.

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 = None
names: Names = PydanticUndefined
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.

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

Overture properties model.

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

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

Convert Overture's place properties to OSM tags.

Used internally by the overturetoosm.process_place function.

Inherited Members
OvertureBaseModel
version
theme
type
id
class ConfidenceError(builtins.Exception):
260class ConfidenceError(Exception):
261    """Confidence error exception.
262
263    This exception is raised when the confidence level of an item is below the
264    user-defined level. It contains the original confidence level and the confidence
265    level of the item.
266
267    Attributes:
268        confidence_level (float): The set confidence level.
269        confidence_item (float): The confidence of the item.
270        message (str): The error message.
271    """
272
273    def __init__(
274        self,
275        confidence_level: float,
276        confidence_item: float,
277        message: str = "Confidence in this item is too low.",
278    ) -> None:
279        """@private"""
280        self.confidence_level = confidence_level
281        self.confidence_item = confidence_item
282        self.message = message
283        super().__init__(message)
284
285    def __str__(self) -> str:
286        """@private"""
287        lev = f"confidence_level={self.confidence_level}"
288        item = f"confidence_item={self.confidence_item}"
289        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):
292class UnmatchedError(Exception):
293    """Unmatched category error.
294
295    This exception is raised when an item's Overture category does not have a
296    corresponding OSM definition. Edit
297    [the OSM Wiki page](https://wiki.openstreetmap.org/wiki/Overture_categories)
298    to add a definition to this category.
299
300    Attributes:
301        category (str): The Overture category that is unmatched.
302        message (str): The error message.
303    """
304
305    def __init__(
306        self, category: str, message: str = "Overture category is unmatched."
307    ) -> None:
308        """@private"""
309        self.category = category
310        self.message = message
311        super().__init__(message)
312
313    def __str__(self) -> str:
314        """@private"""
315        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):
318class BuildingProps(OvertureBaseModel):
319    """Overture building properties.
320
321    Use this model if you want to manipulate the `building` properties yourself.
322    """
323
324    has_parts: bool
325    sources: list[Sources]
326    class_: str | None = Field(alias="class", default=None)
327    subtype: str | None = None
328    names: Names | None = None
329    level: int | None = None
330    height: float | None = None
331    is_underground: bool | None = None
332    num_floors: int | None = Field(serialization_alias="building:levels", default=None)
333    num_floors_underground: int | None = Field(
334        serialization_alias="building:levels:underground", default=None
335    )
336    min_height: float | None = None
337    min_floor: int | None = Field(
338        serialization_alias="building:min_level", default=None
339    )
340    facade_color: str | None = Field(
341        serialization_alias="building:colour", default=None
342    )
343    facade_material: str | None = Field(
344        serialization_alias="building:material", default=None
345    )
346    roof_material: str | None = Field(serialization_alias="roof:material", default=None)
347    roof_shape: str | None = Field(serialization_alias="roof:shape", default=None)
348    roof_direction: str | None = Field(
349        serialization_alias="roof:direction", default=None
350    )
351    roof_orientation: str | None = Field(
352        serialization_alias="roof:orientation", default=None
353    )
354    roof_color: str | None = Field(serialization_alias="roof:colour", default=None)
355    roof_height: float | None = Field(serialization_alias="roof:height", default=None)
356
357    def to_osm(self, confidence: float) -> dict[str, str]:
358        """Convert properties to OSM tags.
359
360        Used internally by`overturetoosm.process_building` function.
361        """
362        new_props = {}
363        confidences = {source.confidence for source in self.sources}
364        if any(conf and conf < confidence for conf in confidences):
365            raise ConfidenceError(confidence, max({i for i in confidences if i}))
366
367        new_props["building"] = self.class_ if self.class_ else "yes"
368
369        new_props["source"] = source_statement(self.sources)
370
371        prop_obj = self.model_dump(exclude_none=True, by_alias=True).items()
372        new_props.update(
373            {k: v for k, v in prop_obj if k.startswith(("roof", "building"))}
374        )
375        new_props.update({k: round(v, 2) for k, v in prop_obj if k.endswith("height")})
376
377        if self.is_underground:
378            new_props["location"] = "underground"
379        if self.names:
380            new_props["name"] = self.names.primary
381        return new_props

Overture building properties.

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

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

Convert properties to OSM tags.

Used internally byoverturetoosm.process_building function.

Inherited Members
OvertureBaseModel
version
theme
type
id
class AddressLevel(pydantic.main.BaseModel):
384class AddressLevel(BaseModel):
385    """Overture address level model."""
386
387    value: str

Overture address level model.

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

Overture address properties.

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

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

Convert properties to OSM tags.

Used internally by overturetoosm.process_address.

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

Return a source statement from a list of sources.

def is_none_or_list_of_nones(value) -> bool:
433def is_none_or_list_of_nones(value) -> bool:
434    """Check whether a given value is either None or a list containing only None values.
435
436    Args:
437        value: The value to check. Can be of any type.
438
439    Returns:
440        bool: True if the value is None or a list containing only None values,
441              False otherwise.
442    """
443    if value is None:
444        return True
445
446    if isinstance(value, list):
447        return len(value) > 0 and all(item is None for item in value)
448
449    return False

Check whether a given value is either None or a list containing only None values.

Arguments:
  • value: The value to check. Can be of any type.
Returns:

bool: True if the value is None or a list containing only None values, False otherwise.