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    license: str | None = None
 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:]}"
 59
 60
 61class RulesVariant(str, Enum):
 62    """Overture name rules variant model."""
 63
 64    alternate = "alternate"
 65    common = "common"
 66    official = "official"
 67    short = "short"
 68
 69
 70class Between(RootModel):
 71    """Model for transportation segment between."""
 72
 73    root: Annotated[list, Field(float, min_length=2, max_length=2)]
 74
 75
 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"
 81
 82
 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)
 88
 89
 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
 99
100
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
115
116
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
141
142
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 {}
159
160
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
173
174
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
189
190
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 _validate_license(self, required_license: str | None) -> None:
211        """Validate that sources meet license requirements.
212
213        Args:
214            required_license: The required license string (e.g., "CDLA").
215                             If None, no validation is performed.
216
217        Raises:
218            LicenseError: If no sources have the required license and at least
219                         one source has a non-null license.
220        """
221        if required_license is None:
222            return
223
224        found_licenses = [s.license for s in self.sources if s.license is not None]
225
226        # If all licenses are null, pass the validation
227        if not found_licenses:
228            return
229
230        # Check if any source has the required license
231        for source in self.sources:
232            if source.license and required_license in source.license:
233                return
234
235        # If we get here, no source has the required license
236        raise LicenseError(required_license, found_licenses)
237
238    def to_osm(
239        self,
240        confidence: float,
241        region_tag: str,
242        unmatched: str,
243        required_license: str | None = None,
244    ) -> dict[str, str]:
245        """Convert Overture's place properties to OSM tags.
246
247        Used internally by the `overturetoosm.process_place` function.
248        """
249        if self.confidence < confidence:
250            raise ConfidenceError(confidence, self.confidence)
251
252        self._validate_license(required_license)
253
254        new_props = {}
255
256        # Categories
257        if self.categories:
258            new_props.update(self.categories.to_osm(unmatched))
259
260        # Names
261        if self.names:
262            new_props.update(self.names.to_osm())
263
264        # Contact information
265        new_props.update(self._process_contact_info())
266
267        # Addresses
268        if self.addresses:
269            new_props.update(self.addresses[0].to_osm(region_tag))
270
271        # Sources
272        new_props["source"] = source_statement(self.sources)
273
274        # Socials and Brand
275        if self.socials:
276            new_props.update(self.socials.to_osm())
277        if self.brand:
278            new_props.update(self.brand.to_osm())
279
280        return new_props
281
282    def _process_contact_info(self) -> dict[str, str]:
283        """Process contact information."""
284        contact_info = {}
285        if not is_none_or_list_of_nones(self.phones):
286            contact_info["phone"] = self.phones[0]
287        if not is_none_or_list_of_nones(self.websites):
288            contact_info["website"] = str(self.websites[0])
289        if not is_none_or_list_of_nones(self.emails):
290            contact_info["email"] = self.emails[0]
291        return contact_info
292
293
294class ConfidenceError(Exception):
295    """Confidence error exception.
296
297    This exception is raised when the confidence level of an item is below the
298    user-defined level. It contains the original confidence level and the confidence
299    level of the item.
300
301    Attributes:
302        confidence_level (float): The set confidence level.
303        confidence_item (float): The confidence of the item.
304        message (str): The error message.
305    """
306
307    def __init__(
308        self,
309        confidence_level: float,
310        confidence_item: float,
311        message: str = "Confidence in this item is too low.",
312    ) -> None:
313        """@private"""
314        self.confidence_level = confidence_level
315        self.confidence_item = confidence_item
316        self.message = message
317        super().__init__(message)
318
319    def __str__(self) -> str:
320        """@private"""
321        lev = f"confidence_level={self.confidence_level}"
322        item = f"confidence_item={self.confidence_item}"
323        return f"""{self.message} {lev}, {item}"""
324
325
326class UnmatchedError(Exception):
327    """Unmatched category error.
328
329    This exception is raised when an item's Overture category does not have a
330    corresponding OSM definition. Edit
331    [the OSM Wiki page](https://wiki.openstreetmap.org/wiki/Overture_categories)
332    to add a definition to this category.
333
334    Attributes:
335        category (str): The Overture category that is unmatched.
336        message (str): The error message.
337    """
338
339    def __init__(
340        self, category: str, message: str = "Overture category is unmatched."
341    ) -> None:
342        """@private"""
343        self.category = category
344        self.message = message
345        super().__init__(message)
346
347    def __str__(self) -> str:
348        """@private"""
349        return f"{self.message} {{category={self.category}}}"
350
351
352class LicenseError(Exception):
353    """License compatibility error.
354
355    This exception is raised when a feature's source licenses do not meet
356    the required license criteria.
357
358    Attributes:
359        required_license (str): The required license string.
360        found_licenses (list[str]): The licenses found in the sources.
361        message (str): The error message.
362    """
363
364    def __init__(
365        self,
366        required_license: str,
367        found_licenses: list[str],
368        message: str = "Feature does not meet license requirements.",
369    ) -> None:
370        """@private"""
371        self.required_license = required_license
372        self.found_licenses = found_licenses
373        self.message = message
374        super().__init__(message)
375
376    def __str__(self) -> str:
377        """@private"""
378        return f"{self.message} {{required_license={self.required_license}, found_licenses={self.found_licenses}}}"
379
380
381class BuildingProps(OvertureBaseModel):
382    """Overture building properties.
383
384    Use this model if you want to manipulate the `building` properties yourself.
385    """
386
387    has_parts: bool
388    sources: list[Sources]
389    class_: str | None = Field(alias="class", default=None)
390    subtype: str | None = None
391    names: Names | None = None
392    level: int | None = None
393    height: float | None = None
394    is_underground: bool | None = None
395    num_floors: int | None = Field(serialization_alias="building:levels", default=None)
396    num_floors_underground: int | None = Field(
397        serialization_alias="building:levels:underground", default=None
398    )
399    min_height: float | None = None
400    min_floor: int | None = Field(
401        serialization_alias="building:min_level", default=None
402    )
403    facade_color: str | None = Field(
404        serialization_alias="building:colour", default=None
405    )
406    facade_material: str | None = Field(
407        serialization_alias="building:material", default=None
408    )
409    roof_material: str | None = Field(serialization_alias="roof:material", default=None)
410    roof_shape: str | None = Field(serialization_alias="roof:shape", default=None)
411    roof_direction: str | None = Field(
412        serialization_alias="roof:direction", default=None
413    )
414    roof_orientation: str | None = Field(
415        serialization_alias="roof:orientation", default=None
416    )
417    roof_color: str | None = Field(serialization_alias="roof:colour", default=None)
418    roof_height: float | None = Field(serialization_alias="roof:height", default=None)
419
420    def _validate_license(self, required_license: str | None) -> None:
421        """Validate that sources meet license requirements.
422
423        Args:
424            required_license: The required license string (e.g., "CDLA").
425                             If None, no validation is performed.
426
427        Raises:
428            LicenseError: If no sources have the required license and at least
429                         one source has a non-null license.
430        """
431        if required_license is None:
432            return
433
434        found_licenses = [s.license for s in self.sources if s.license is not None]
435
436        # If all licenses are null, pass the validation
437        if not found_licenses:
438            return
439
440        # Check if any source has the required license
441        for source in self.sources:
442            if source.license and required_license in source.license:
443                return
444
445        # If we get here, no source has the required license
446        raise LicenseError(required_license, found_licenses)
447
448    def to_osm(
449        self, confidence: float, required_license: str | None = None
450    ) -> dict[str, str]:
451        """Convert properties to OSM tags.
452
453        Used internally by`overturetoosm.process_building` function.
454        """
455        new_props = {}
456        confidences = {source.confidence for source in self.sources}
457        if any(conf and conf < confidence for conf in confidences):
458            raise ConfidenceError(confidence, max({i for i in confidences if i}))
459
460        self._validate_license(required_license)
461
462        new_props["building"] = self.class_ if self.class_ else "yes"
463
464        new_props["source"] = source_statement(self.sources)
465
466        prop_obj = self.model_dump(exclude_none=True, by_alias=True).items()
467        new_props.update(
468            {k: v for k, v in prop_obj if k.startswith(("roof", "building"))}
469        )
470        new_props.update({k: round(v, 2) for k, v in prop_obj if k.endswith("height")})
471
472        if self.is_underground:
473            new_props["location"] = "underground"
474        if self.names:
475            new_props["name"] = self.names.primary
476        return new_props
477
478
479class AddressLevel(BaseModel):
480    """Overture address level model."""
481
482    value: str
483
484
485class AddressProps(OvertureBaseModel):
486    """Overture address properties.
487
488    Use this model directly if you want to manipulate the `address` properties yourself.
489    """
490
491    number: str | None = Field(serialization_alias="addr:housenumber", default=None)
492    street: str | None = Field(serialization_alias="addr:street", default=None)
493    unit: str | None = Field(serialization_alias="addr:unit", default=None)
494    postcode: str | None = Field(serialization_alias="addr:postcode", default=None)
495    postal_city: str | None = Field(serialization_alias="addr:city", default=None)
496    country: str | None = Field(serialization_alias="addr:country", default=None)
497    address_levels: (
498        None | (Annotated[list[AddressLevel], Field(min_length=1, max_length=5)])
499    ) = Field(default_factory=list)
500    sources: list[Sources]
501
502    def _validate_license(self, required_license: str | None) -> None:
503        """Validate that sources meet license requirements.
504
505        Args:
506            required_license: The required license string (e.g., "CDLA").
507                             If None, no validation is performed.
508
509        Raises:
510            LicenseError: If no sources have the required license and at least
511                         one source has a non-null license.
512        """
513        if required_license is None:
514            return
515
516        found_licenses = [s.license for s in self.sources if s.license is not None]
517
518        # If all licenses are null, pass the validation
519        if not found_licenses:
520            return
521
522        # Check if any source has the required license
523        for source in self.sources:
524            if source.license and required_license in source.license:
525                return
526
527        # If we get here, no source has the required license
528        raise LicenseError(required_license, found_licenses)
529
530    def to_osm(self, style: str, required_license: str | None = None) -> dict[str, str]:
531        """Convert properties to OSM tags.
532
533        Used internally by `overturetoosm.process_address`.
534        """
535        self._validate_license(required_license)
536
537        obj_dict = {
538            k: v
539            for k, v in self.model_dump(exclude_none=True, by_alias=True).items()
540            if k.startswith("addr:")
541        }
542        obj_dict["source"] = source_statement(self.sources)
543
544        if self.address_levels and len(self.address_levels) > 0 and style == "US":
545            obj_dict["addr:state"] = str(self.address_levels[0].value)
546
547        return obj_dict
548
549
550def source_statement(source: list[Sources]) -> str:
551    """Return a source statement from a list of sources."""
552    return (
553        ", ".join(sorted({i.dataset.strip(", ") for i in source}))
554        + " via overturetoosm"
555    )
556
557
558def is_none_or_list_of_nones(value) -> bool:
559    """Check whether a given value is either None or a list containing only None values.
560
561    Args:
562        value: The value to check. Can be of any type.
563
564    Returns:
565        bool: True if the value is None or a list containing only None values,
566              False otherwise.
567    """
568    if value is None:
569        return True
570
571    if isinstance(value, list):
572        return len(value) > 0 and all(item is None for item in value)
573
574    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    license: str | None = None
41    record_id: str | None = None
42    confidence: float | None = Field(ge=0.0, le=1.0)
43    update_time: str | None = None
44
45    @field_validator("confidence")
46    @classmethod
47    def set_default_if_none(cls, v: float) -> float:
48        """@private"""
49        return v if v is not None else 0.0
50
51    def get_osm_link(self) -> str | None:
52        """Return the OSM link for the source."""
53        if (
54            self.record_id
55            and self.record_id.startswith(("n", "w", "r"))
56            and self.dataset == "OpenStreetMap"
57        ):
58            type_dict = {"n": "node", "w": "way", "r": "relation"}
59            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
license: str | None = None
record_id: str | None = None
confidence: float | None = PydanticUndefined
update_time: str | None = None
class RulesVariant(builtins.str, enum.Enum):
62class RulesVariant(str, Enum):
63    """Overture name rules variant model."""
64
65    alternate = "alternate"
66    common = "common"
67    official = "official"
68    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]):
71class Between(RootModel):
72    """Model for transportation segment between."""
73
74    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):
77class Mode(str, Enum):
78    """Model for political perspectives from which a named feature is viewed."""
79
80    accepted_by = "accepted_by"
81    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):
84class Perspectives(BaseModel):
85    """Model for political perspectives from which a named feature is viewed."""
86
87    mode: Mode
88    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):
91class Rules(BaseModel):
92    """Overture name rules model."""
93
94    variant: RulesVariant
95    language: str | None = None
96    value: str
97    between: Between | None = None
98    side: str | None = None
99    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):
102class Names(BaseModel):
103    """Overture names model."""
104
105    primary: str
106    common: list[tuple[str, str]] | None
107    rules: list[Rules] | None
108
109    def to_osm(self) -> dict[str, str]:
110        """Convert names to OSM tags."""
111        names = {}
112        if self.primary:
113            names["name"] = self.primary
114
115        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]:
109    def to_osm(self) -> dict[str, str]:
110        """Convert names to OSM tags."""
111        names = {}
112        if self.primary:
113            names["name"] = self.primary
114
115        return names

Convert names to OSM tags.

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

Convert address to OSM tags.

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

Overture categories model.

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

Convert categories to OSM tags.

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

Overture brand model.

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

Convert brand properties to OSM tags.

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

Overture socials model.

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

Convert socials properties to OSM tags.

class PlaceProps(OvertureBaseModel):
192class PlaceProps(OvertureBaseModel):
193    """Overture properties model.
194
195    Use this model directly if you want to manipulate the `place` properties yourself.
196    """
197
198    model_config = ConfigDict(extra="ignore")
199
200    sources: list[Sources]
201    names: Names
202    brand: Brand | None = None
203    categories: Categories | None = None
204    confidence: float = Field(ge=0.0, le=1.0)
205    websites: list[str | None] | None = None
206    socials: Socials | None = None
207    emails: list[str | None] | None = None
208    phones: list[str | None] | None = None
209    addresses: list[PlaceAddress]
210
211    def _validate_license(self, required_license: str | None) -> None:
212        """Validate that sources meet license requirements.
213
214        Args:
215            required_license: The required license string (e.g., "CDLA").
216                             If None, no validation is performed.
217
218        Raises:
219            LicenseError: If no sources have the required license and at least
220                         one source has a non-null license.
221        """
222        if required_license is None:
223            return
224
225        found_licenses = [s.license for s in self.sources if s.license is not None]
226
227        # If all licenses are null, pass the validation
228        if not found_licenses:
229            return
230
231        # Check if any source has the required license
232        for source in self.sources:
233            if source.license and required_license in source.license:
234                return
235
236        # If we get here, no source has the required license
237        raise LicenseError(required_license, found_licenses)
238
239    def to_osm(
240        self,
241        confidence: float,
242        region_tag: str,
243        unmatched: str,
244        required_license: str | None = None,
245    ) -> dict[str, str]:
246        """Convert Overture's place properties to OSM tags.
247
248        Used internally by the `overturetoosm.process_place` function.
249        """
250        if self.confidence < confidence:
251            raise ConfidenceError(confidence, self.confidence)
252
253        self._validate_license(required_license)
254
255        new_props = {}
256
257        # Categories
258        if self.categories:
259            new_props.update(self.categories.to_osm(unmatched))
260
261        # Names
262        if self.names:
263            new_props.update(self.names.to_osm())
264
265        # Contact information
266        new_props.update(self._process_contact_info())
267
268        # Addresses
269        if self.addresses:
270            new_props.update(self.addresses[0].to_osm(region_tag))
271
272        # Sources
273        new_props["source"] = source_statement(self.sources)
274
275        # Socials and Brand
276        if self.socials:
277            new_props.update(self.socials.to_osm())
278        if self.brand:
279            new_props.update(self.brand.to_osm())
280
281        return new_props
282
283    def _process_contact_info(self) -> dict[str, str]:
284        """Process contact information."""
285        contact_info = {}
286        if not is_none_or_list_of_nones(self.phones):
287            contact_info["phone"] = self.phones[0]
288        if not is_none_or_list_of_nones(self.websites):
289            contact_info["website"] = str(self.websites[0])
290        if not is_none_or_list_of_nones(self.emails):
291            contact_info["email"] = self.emails[0]
292        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, required_license: str | None = None) -> dict[str, str]:
239    def to_osm(
240        self,
241        confidence: float,
242        region_tag: str,
243        unmatched: str,
244        required_license: str | None = None,
245    ) -> dict[str, str]:
246        """Convert Overture's place properties to OSM tags.
247
248        Used internally by the `overturetoosm.process_place` function.
249        """
250        if self.confidence < confidence:
251            raise ConfidenceError(confidence, self.confidence)
252
253        self._validate_license(required_license)
254
255        new_props = {}
256
257        # Categories
258        if self.categories:
259            new_props.update(self.categories.to_osm(unmatched))
260
261        # Names
262        if self.names:
263            new_props.update(self.names.to_osm())
264
265        # Contact information
266        new_props.update(self._process_contact_info())
267
268        # Addresses
269        if self.addresses:
270            new_props.update(self.addresses[0].to_osm(region_tag))
271
272        # Sources
273        new_props["source"] = source_statement(self.sources)
274
275        # Socials and Brand
276        if self.socials:
277            new_props.update(self.socials.to_osm())
278        if self.brand:
279            new_props.update(self.brand.to_osm())
280
281        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):
295class ConfidenceError(Exception):
296    """Confidence error exception.
297
298    This exception is raised when the confidence level of an item is below the
299    user-defined level. It contains the original confidence level and the confidence
300    level of the item.
301
302    Attributes:
303        confidence_level (float): The set confidence level.
304        confidence_item (float): The confidence of the item.
305        message (str): The error message.
306    """
307
308    def __init__(
309        self,
310        confidence_level: float,
311        confidence_item: float,
312        message: str = "Confidence in this item is too low.",
313    ) -> None:
314        """@private"""
315        self.confidence_level = confidence_level
316        self.confidence_item = confidence_item
317        self.message = message
318        super().__init__(message)
319
320    def __str__(self) -> str:
321        """@private"""
322        lev = f"confidence_level={self.confidence_level}"
323        item = f"confidence_item={self.confidence_item}"
324        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):
327class UnmatchedError(Exception):
328    """Unmatched category error.
329
330    This exception is raised when an item's Overture category does not have a
331    corresponding OSM definition. Edit
332    [the OSM Wiki page](https://wiki.openstreetmap.org/wiki/Overture_categories)
333    to add a definition to this category.
334
335    Attributes:
336        category (str): The Overture category that is unmatched.
337        message (str): The error message.
338    """
339
340    def __init__(
341        self, category: str, message: str = "Overture category is unmatched."
342    ) -> None:
343        """@private"""
344        self.category = category
345        self.message = message
346        super().__init__(message)
347
348    def __str__(self) -> str:
349        """@private"""
350        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 LicenseError(builtins.Exception):
353class LicenseError(Exception):
354    """License compatibility error.
355
356    This exception is raised when a feature's source licenses do not meet
357    the required license criteria.
358
359    Attributes:
360        required_license (str): The required license string.
361        found_licenses (list[str]): The licenses found in the sources.
362        message (str): The error message.
363    """
364
365    def __init__(
366        self,
367        required_license: str,
368        found_licenses: list[str],
369        message: str = "Feature does not meet license requirements.",
370    ) -> None:
371        """@private"""
372        self.required_license = required_license
373        self.found_licenses = found_licenses
374        self.message = message
375        super().__init__(message)
376
377    def __str__(self) -> str:
378        """@private"""
379        return f"{self.message} {{required_license={self.required_license}, found_licenses={self.found_licenses}}}"

License compatibility error.

This exception is raised when a feature's source licenses do not meet the required license criteria.

Attributes:
  • required_license (str): The required license string.
  • found_licenses (list[str]): The licenses found in the sources.
  • message (str): The error message.
required_license
found_licenses
message
class BuildingProps(OvertureBaseModel):
382class BuildingProps(OvertureBaseModel):
383    """Overture building properties.
384
385    Use this model if you want to manipulate the `building` properties yourself.
386    """
387
388    has_parts: bool
389    sources: list[Sources]
390    class_: str | None = Field(alias="class", default=None)
391    subtype: str | None = None
392    names: Names | None = None
393    level: int | None = None
394    height: float | None = None
395    is_underground: bool | None = None
396    num_floors: int | None = Field(serialization_alias="building:levels", default=None)
397    num_floors_underground: int | None = Field(
398        serialization_alias="building:levels:underground", default=None
399    )
400    min_height: float | None = None
401    min_floor: int | None = Field(
402        serialization_alias="building:min_level", default=None
403    )
404    facade_color: str | None = Field(
405        serialization_alias="building:colour", default=None
406    )
407    facade_material: str | None = Field(
408        serialization_alias="building:material", default=None
409    )
410    roof_material: str | None = Field(serialization_alias="roof:material", default=None)
411    roof_shape: str | None = Field(serialization_alias="roof:shape", default=None)
412    roof_direction: str | None = Field(
413        serialization_alias="roof:direction", default=None
414    )
415    roof_orientation: str | None = Field(
416        serialization_alias="roof:orientation", default=None
417    )
418    roof_color: str | None = Field(serialization_alias="roof:colour", default=None)
419    roof_height: float | None = Field(serialization_alias="roof:height", default=None)
420
421    def _validate_license(self, required_license: str | None) -> None:
422        """Validate that sources meet license requirements.
423
424        Args:
425            required_license: The required license string (e.g., "CDLA").
426                             If None, no validation is performed.
427
428        Raises:
429            LicenseError: If no sources have the required license and at least
430                         one source has a non-null license.
431        """
432        if required_license is None:
433            return
434
435        found_licenses = [s.license for s in self.sources if s.license is not None]
436
437        # If all licenses are null, pass the validation
438        if not found_licenses:
439            return
440
441        # Check if any source has the required license
442        for source in self.sources:
443            if source.license and required_license in source.license:
444                return
445
446        # If we get here, no source has the required license
447        raise LicenseError(required_license, found_licenses)
448
449    def to_osm(
450        self, confidence: float, required_license: str | None = None
451    ) -> dict[str, str]:
452        """Convert properties to OSM tags.
453
454        Used internally by`overturetoosm.process_building` function.
455        """
456        new_props = {}
457        confidences = {source.confidence for source in self.sources}
458        if any(conf and conf < confidence for conf in confidences):
459            raise ConfidenceError(confidence, max({i for i in confidences if i}))
460
461        self._validate_license(required_license)
462
463        new_props["building"] = self.class_ if self.class_ else "yes"
464
465        new_props["source"] = source_statement(self.sources)
466
467        prop_obj = self.model_dump(exclude_none=True, by_alias=True).items()
468        new_props.update(
469            {k: v for k, v in prop_obj if k.startswith(("roof", "building"))}
470        )
471        new_props.update({k: round(v, 2) for k, v in prop_obj if k.endswith("height")})
472
473        if self.is_underground:
474            new_props["location"] = "underground"
475        if self.names:
476            new_props["name"] = self.names.primary
477        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, required_license: str | None = None) -> dict[str, str]:
449    def to_osm(
450        self, confidence: float, required_license: str | None = None
451    ) -> dict[str, str]:
452        """Convert properties to OSM tags.
453
454        Used internally by`overturetoosm.process_building` function.
455        """
456        new_props = {}
457        confidences = {source.confidence for source in self.sources}
458        if any(conf and conf < confidence for conf in confidences):
459            raise ConfidenceError(confidence, max({i for i in confidences if i}))
460
461        self._validate_license(required_license)
462
463        new_props["building"] = self.class_ if self.class_ else "yes"
464
465        new_props["source"] = source_statement(self.sources)
466
467        prop_obj = self.model_dump(exclude_none=True, by_alias=True).items()
468        new_props.update(
469            {k: v for k, v in prop_obj if k.startswith(("roof", "building"))}
470        )
471        new_props.update({k: round(v, 2) for k, v in prop_obj if k.endswith("height")})
472
473        if self.is_underground:
474            new_props["location"] = "underground"
475        if self.names:
476            new_props["name"] = self.names.primary
477        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):
480class AddressLevel(BaseModel):
481    """Overture address level model."""
482
483    value: str

Overture address level model.

value: str = PydanticUndefined
class AddressProps(OvertureBaseModel):
486class AddressProps(OvertureBaseModel):
487    """Overture address properties.
488
489    Use this model directly if you want to manipulate the `address` properties yourself.
490    """
491
492    number: str | None = Field(serialization_alias="addr:housenumber", default=None)
493    street: str | None = Field(serialization_alias="addr:street", default=None)
494    unit: str | None = Field(serialization_alias="addr:unit", default=None)
495    postcode: str | None = Field(serialization_alias="addr:postcode", default=None)
496    postal_city: str | None = Field(serialization_alias="addr:city", default=None)
497    country: str | None = Field(serialization_alias="addr:country", default=None)
498    address_levels: (
499        None | (Annotated[list[AddressLevel], Field(min_length=1, max_length=5)])
500    ) = Field(default_factory=list)
501    sources: list[Sources]
502
503    def _validate_license(self, required_license: str | None) -> None:
504        """Validate that sources meet license requirements.
505
506        Args:
507            required_license: The required license string (e.g., "CDLA").
508                             If None, no validation is performed.
509
510        Raises:
511            LicenseError: If no sources have the required license and at least
512                         one source has a non-null license.
513        """
514        if required_license is None:
515            return
516
517        found_licenses = [s.license for s in self.sources if s.license is not None]
518
519        # If all licenses are null, pass the validation
520        if not found_licenses:
521            return
522
523        # Check if any source has the required license
524        for source in self.sources:
525            if source.license and required_license in source.license:
526                return
527
528        # If we get here, no source has the required license
529        raise LicenseError(required_license, found_licenses)
530
531    def to_osm(self, style: str, required_license: str | None = None) -> dict[str, str]:
532        """Convert properties to OSM tags.
533
534        Used internally by `overturetoosm.process_address`.
535        """
536        self._validate_license(required_license)
537
538        obj_dict = {
539            k: v
540            for k, v in self.model_dump(exclude_none=True, by_alias=True).items()
541            if k.startswith("addr:")
542        }
543        obj_dict["source"] = source_statement(self.sources)
544
545        if self.address_levels and len(self.address_levels) > 0 and style == "US":
546            obj_dict["addr:state"] = str(self.address_levels[0].value)
547
548        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, required_license: str | None = None) -> dict[str, str]:
531    def to_osm(self, style: str, required_license: str | None = None) -> dict[str, str]:
532        """Convert properties to OSM tags.
533
534        Used internally by `overturetoosm.process_address`.
535        """
536        self._validate_license(required_license)
537
538        obj_dict = {
539            k: v
540            for k, v in self.model_dump(exclude_none=True, by_alias=True).items()
541            if k.startswith("addr:")
542        }
543        obj_dict["source"] = source_statement(self.sources)
544
545        if self.address_levels and len(self.address_levels) > 0 and style == "US":
546            obj_dict["addr:state"] = str(self.address_levels[0].value)
547
548        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:
551def source_statement(source: list[Sources]) -> str:
552    """Return a source statement from a list of sources."""
553    return (
554        ", ".join(sorted({i.dataset.strip(", ") for i in source}))
555        + " via overturetoosm"
556    )

Return a source statement from a list of sources.

def is_none_or_list_of_nones(value) -> bool:
559def is_none_or_list_of_nones(value) -> bool:
560    """Check whether a given value is either None or a list containing only None values.
561
562    Args:
563        value: The value to check. Can be of any type.
564
565    Returns:
566        bool: True if the value is None or a list containing only None values,
567              False otherwise.
568    """
569    if value is None:
570        return True
571
572    if isinstance(value, list):
573        return len(value) > 0 and all(item is None for item in value)
574
575    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.