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
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.
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.
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.
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:]}"
Return the OSM link for the source.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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
Overture address level model.
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.
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
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.
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.