From 3ad745330883d1917d91bb4b19fc0b8b2dff81a0 Mon Sep 17 00:00:00 2001 From: Will Date: Thu, 1 Aug 2024 16:50:38 -0400 Subject: [PATCH] Adding support for addresses --- pyproject.toml | 1 - src/overturetoosm/addresses.py | 29 ++++++++++++++++ src/overturetoosm/objects.py | 63 +++++++++++++++++++++++++--------- src/overturetoosm/places.py | 4 +-- tests/test_places.py | 12 +++---- 5 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 src/overturetoosm/addresses.py diff --git a/pyproject.toml b/pyproject.toml index f7ed117..6a295ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", - "Framework :: Hatch", "License :: OSI Approved :: MIT License", "Typing :: Typed", ] diff --git a/src/overturetoosm/addresses.py b/src/overturetoosm/addresses.py new file mode 100644 index 0000000..fedac26 --- /dev/null +++ b/src/overturetoosm/addresses.py @@ -0,0 +1,29 @@ +from typing import Dict, Literal +from .objects import AddressProps + + +def process_address( + props: dict, + style: Literal["US"] = "US", +) -> Dict[str, str]: + """Convert Overture's address properties to OSM tags. + + Args: + props (dict): The feature properties from the Overture GeoJSON. + style (str, optional): How to handle the `address_levels` field. Open a pull request or issue to add support for other regions. Defaults to "US". + + Returns: + Dict[str, str]: The reshaped and converted properties in OSM's flat str:str schema. + """ + bad_tags = ["version", "theme", "type", "address_levels"] + print(props) + prop = AddressProps(**props) + obj_dict = prop.model_dump(exclude_none=True, by_alias=True) + if prop.address_levels: + if style == "US": + obj_dict["addr:state"] = prop.address_levels[0]["value"] + + for tag in bad_tags: + obj_dict.pop(tag, None) + + return obj_dict diff --git a/src/overturetoosm/objects.py b/src/overturetoosm/objects.py index e7fe458..340976b 100644 --- a/src/overturetoosm/objects.py +++ b/src/overturetoosm/objects.py @@ -1,19 +1,20 @@ """Pydantic models needed throughout the project.""" from typing import Dict, List, Optional -import pydantic +from pydantic import AnyUrl, BaseModel, Field -class Sources(pydantic.BaseModel): +class Sources(BaseModel): """Overture sources model.""" property: str dataset: str record_id: Optional[str] = None - confidence: float = pydantic.Field(default=0.0) + confidence: float = Field(default=0.0) + update_time: Optional[str] = None -class Names(pydantic.BaseModel): +class Names(BaseModel): """Overture names model.""" primary: str @@ -21,48 +22,57 @@ class Names(pydantic.BaseModel): rules: Optional[List[Dict[str, str]]] -class Addresses(pydantic.BaseModel): +class Address(BaseModel): """Overture addresses model.""" freeform: Optional[str] locality: Optional[str] postcode: Optional[str] region: Optional[str] - country: Optional[str] + country: Optional[str] = Field(pattern=r"^[A-Z]{2}$") -class Categories(pydantic.BaseModel): +class Categories(BaseModel): """Overture categories model.""" main: str alternate: Optional[List[str]] -class Brand(pydantic.BaseModel): +class Brand(BaseModel): """Overture brand model.""" - wikidata: Optional[str] + wikidata: Optional[str] = Field(pattern=r"Q[0-9]+") names: Names -class PlaceProps(pydantic.BaseModel): +class OvertureBase(BaseModel): + """Overture base model.""" + + theme: str + type: str + version: int + + +class PlaceProps(BaseModel): """Overture properties model. Use this model directly if you want to manipulate the `place` properties yourself. """ - id: str + id: Optional[str] = None version: int update_time: str sources: List[Sources] names: Names brand: Optional[Brand] = None categories: Optional[Categories] = None - confidence: float = pydantic.Field(ge=0.0, le=1.0) - websites: Optional[List[str]] = None - socials: Optional[List[str]] = None + confidence: float = Field(ge=0.0, le=1.0) + websites: Optional[List[AnyUrl]] = None + socials: Optional[List[AnyUrl]] = None + emails: Optional[List[str]] = None phones: Optional[List[str]] = None - addresses: List[Addresses] + addresses: List[Address] class ConfidenceError(Exception): @@ -126,14 +136,14 @@ def __str__(self) -> str: return f"{self.message} {{category={self.category}}}" -class BuildingProps(pydantic.BaseModel): +class BuildingProps(BaseModel): """Overture building properties. Use this model directly if you want to manipulate the `building` properties yourself. """ version: int - class_: str = pydantic.Field(alias="class") + class_: str = Field(alias="class") subtype: str sources: List[Sources] height: Optional[float] = None @@ -150,3 +160,22 @@ class BuildingProps(pydantic.BaseModel): roof_orientation: Optional[str] = None roof_color: Optional[str] = None roof_height: Optional[float] = None + + +class AddressLevel(BaseModel): + """Overture address level model.""" + + value: str + + +class AddressProps(OvertureBase): + """Overture address properties. + + Use this model directly if you want to manipulate the `address` properties yourself. + """ + + number: Optional[str] = Field(serialization_alias="addr:housenumber") + street: Optional[str] = Field(serialization_alias="addr:street") + postcode: Optional[str] = Field(serialization_alias="addr:postcode") + country: Optional[str] = Field(serialization_alias="addr:country") + address_levels: Optional[List[AddressLevel]] = None diff --git a/src/overturetoosm/places.py b/src/overturetoosm/places.py index 556d530..149860a 100644 --- a/src/overturetoosm/places.py +++ b/src/overturetoosm/places.py @@ -89,9 +89,9 @@ def process_place( if prop_obj.socials is not None: for social in prop_obj.socials: - if "facebook" in social: + if "facebook" in str(social): new_props["contact:facebook"] = social - elif "twitter" in social: + elif "twitter" in str(social): new_props["contact:twitter"] = social if prop_obj.brand: diff --git a/tests/test_places.py b/tests/test_places.py index 06805a0..6c50cba 100644 --- a/tests/test_places.py +++ b/tests/test_places.py @@ -22,13 +22,13 @@ def clean_fix() -> Dict[str, Any]: "addr:city": "City", "addr:postcode": "12345", "addr:state": "CA", - "addr:country": "Country", + "addr:country": "US", "phone": "+1234567890", "website": "https://example.com", "source": "dataset1 via overturetoosm", "office": "lawyer", "lawyer": "notary", - "contact:facebook": "www.facebook.com/example", + "contact:facebook": "https://www.facebook.com/example", } @@ -72,7 +72,7 @@ def geojson_fix() -> Dict[str, Any]: }, "confidence": 0.8, "websites": ["https://example.com"], - "socials": ["www.facebook.com/example"], + "socials": ["https://www.facebook.com/example"], "phones": ["+1234567890"], "addresses": [ { @@ -80,7 +80,7 @@ def geojson_fix() -> Dict[str, Any]: "locality": "City", "postcode": "12345", "region": "CA", - "country": "Country", + "country": "US", } ], }, @@ -123,7 +123,7 @@ def props_fix() -> Dict[str, Any]: }, "confidence": 0.8, "websites": ["https://example.com"], - "socials": ["www.facebook.com/example"], + "socials": ["https://www.facebook.com/example"], "phones": ["+1234567890"], "addresses": [ { @@ -131,7 +131,7 @@ def props_fix() -> Dict[str, Any]: "locality": "City", "postcode": "12345", "region": "CA", - "country": "Country", + "country": "US", } ], }