endpoint setup

This commit is contained in:
exolonConfidental
2026-02-08 11:18:47 +05:30
parent 8fb3b7cf67
commit a77788fc47
33 changed files with 932 additions and 352 deletions

View File

@@ -21,12 +21,35 @@ from app.db.models.base import Base
from app.db.models.location import Location from app.db.models.location import Location
from app.db.models.owner import Owner from app.db.models.owner import Owner
from app.db.models.property import Property from app.db.models.property import Property
from app.db.models.location import Location from app.db.models.insurance_details import InsuranceDetails
target_metadata = Base.metadata target_metadata = Base.metadata
# ---- IMPORTANT: IGNORE POSTGIS TABLES ----
def include_object(object, name, type_, reflected, compare_to):
POSTGIS_TABLES = {
"spatial_ref_sys",
"layer",
"topology",
"geography_columns",
"geometry_columns",
"raster_columns",
"raster_overviews",
}
# ignore PostGIS internal tables
if type_ == "table" and name in POSTGIS_TABLES:
return False
# ignore entire topology schema
if getattr(object, "schema", None) in {"topology", "tiger", "tiger_data"}:
return False
return True
# ---- OFFLINE MIGRATIONS ----
def run_migrations_offline(): def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url") url = config.get_main_option("sqlalchemy.url")
@@ -36,12 +59,15 @@ def run_migrations_offline():
target_metadata=target_metadata, target_metadata=target_metadata,
literal_binds=True, literal_binds=True,
dialect_opts={"paramstyle": "named"}, dialect_opts={"paramstyle": "named"},
include_object=include_object, # ✅ applied here
compare_type=True,
) )
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
# ---- ONLINE MIGRATIONS (ASYNC) ----
async def run_migrations_online(): async def run_migrations_online():
connectable = async_engine_from_config( connectable = async_engine_from_config(
@@ -56,6 +82,8 @@ async def run_migrations_online():
context.configure( context.configure(
connection=connection, connection=connection,
target_metadata=target_metadata, target_metadata=target_metadata,
include_object=include_object, # ✅ THIS WAS MISSING
compare_type=True,
) )
with context.begin_transaction(): with context.begin_transaction():
@@ -65,6 +93,8 @@ async def run_migrations_online():
await connectable.dispose() await connectable.dispose()
# ---- ENTRYPOINT ----
def run(): def run():
if context.is_offline_mode(): if context.is_offline_mode():
@@ -74,4 +104,4 @@ def run():
asyncio.run(run_migrations_online()) asyncio.run(run_migrations_online())
run() run()

View File

@@ -0,0 +1,60 @@
"""add insurance details table
Revision ID: 471bdc3c5b51
Revises: b7538fce8343
Create Date: 2026-02-07 23:47:44.253489
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '471bdc3c5b51'
down_revision: Union[str, Sequence[str], None] = 'b7538fce8343'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('insurance_details',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('property_id', sa.Integer(), nullable=False),
sa.Column('insurance_company', sa.String(length=150), nullable=False),
sa.Column('claim_number', sa.String(length=100), nullable=False),
sa.Column('date_of_loss', sa.Date(), nullable=False),
sa.Column('adjuster_name', sa.String(length=150), nullable=True),
sa.Column('adjuster_phone', sa.String(length=30), nullable=True),
sa.Column('adjuster_email', sa.String(length=150), nullable=True),
sa.Column('claim_filed', sa.Boolean(), nullable=False),
sa.Column('claim_approved', sa.Boolean(), nullable=False),
sa.Column('policy_number', sa.String(length=100), nullable=True),
sa.Column('coverage_type', sa.String(length=50), nullable=True),
sa.Column('claim_type', sa.String(length=50), nullable=True),
sa.Column('deductible_amount', sa.Integer(), nullable=True),
sa.Column('claim_amount', sa.Integer(), nullable=True),
sa.Column('approved_amount', sa.Integer(), nullable=True),
sa.Column('payment_status', sa.String(length=50), nullable=True),
sa.Column('date_claim_filed', sa.Date(), nullable=True),
sa.Column('date_claim_closed', sa.Date(), nullable=True),
sa.Column('insurance_agent_name', sa.String(length=150), nullable=True),
sa.Column('insurance_agent_phone', sa.String(length=30), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['property_id'], ['properties.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('claim_number')
)
op.create_index(op.f('ix_insurance_details_property_id'), 'insurance_details', ['property_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_insurance_details_property_id'), table_name='insurance_details')
op.drop_table('insurance_details')
# ### end Alembic commands ###

View File

@@ -0,0 +1,73 @@
"""add geom column
Revision ID: b7538fce8343
Revises: 6cd12cae8c96
Create Date: 2026-02-07 14:33:51.832269
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from geoalchemy2 import Geography
# revision identifiers, used by Alembic.
revision: str = 'b7538fce8343'
down_revision: Union[str, Sequence[str], None] = '6cd12cae8c96'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
# op.drop_table('layer')
# op.drop_table('spatial_ref_sys')
# op.drop_table('topology')
op.add_column('locations', sa.Column('geom',Geography(geometry_type='POINT', srid=4326, dimension=2, from_text='ST_GeogFromText', name='geography', nullable=False), nullable=False))
op.execute(
"CREATE INDEX IF NOT EXISTS idx_locations_geom ON locations USING gist (geom);"
)
op.create_unique_constraint('uq_osm_location', 'locations', ['osm_type', 'osm_id'])
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('uq_osm_location', 'locations', type_='unique')
op.drop_index('idx_locations_geom', table_name='locations', postgresql_using='gist')
op.drop_column('locations', 'geom')
# op.create_table('topology',
# sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
# sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False),
# sa.Column('srid', sa.INTEGER(), autoincrement=False, nullable=False),
# sa.Column('precision', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False),
# sa.Column('hasz', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False),
# sa.Column('useslargeids', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False),
# sa.PrimaryKeyConstraint('id', name=op.f('topology_pkey')),
# sa.UniqueConstraint('name', name=op.f('topology_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
# )
# op.create_table('spatial_ref_sys',
# sa.Column('srid', sa.INTEGER(), autoincrement=False, nullable=False),
# sa.Column('auth_name', sa.VARCHAR(length=256), autoincrement=False, nullable=True),
# sa.Column('auth_srid', sa.INTEGER(), autoincrement=False, nullable=True),
# sa.Column('srtext', sa.VARCHAR(length=2048), autoincrement=False, nullable=True),
# sa.Column('proj4text', sa.VARCHAR(length=2048), autoincrement=False, nullable=True),
# sa.CheckConstraint('srid > 0 AND srid <= 998999', name=op.f('spatial_ref_sys_srid_check')),
# sa.PrimaryKeyConstraint('srid', name=op.f('spatial_ref_sys_pkey'))
# )
# op.create_table('layer',
# sa.Column('topology_id', sa.INTEGER(), autoincrement=False, nullable=False),
# sa.Column('layer_id', sa.INTEGER(), autoincrement=False, nullable=False),
# sa.Column('schema_name', sa.VARCHAR(), autoincrement=False, nullable=False),
# sa.Column('table_name', sa.VARCHAR(), autoincrement=False, nullable=False),
# sa.Column('feature_column', sa.VARCHAR(), autoincrement=False, nullable=False),
# sa.Column('feature_type', sa.INTEGER(), autoincrement=False, nullable=False),
# sa.Column('level', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=False),
# sa.Column('child_id', sa.INTEGER(), autoincrement=False, nullable=True),
# sa.ForeignKeyConstraint(['topology_id'], ['topology.id'], name=op.f('layer_topology_id_fkey')),
# sa.PrimaryKeyConstraint('topology_id', 'layer_id', name=op.f('layer_pkey')),
# sa.UniqueConstraint('schema_name', 'table_name', 'feature_column', name=op.f('layer_schema_name_table_name_feature_column_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
# )
# ### end Alembic commands ###

View File

@@ -0,0 +1,40 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_async_session
from app.services.location_service import LocationService
from app.schemas.map_schema import MapQueryResponse
router = APIRouter(prefix="/map", tags=["Map"])
@router.get("/query", response_model=MapQueryResponse)
async def query_nearby_locations(
lat: float = Query(..., ge=-90, le=90),
lon: float = Query(..., ge=-180, le=180),
radius: int = Query(2000, gt=0, le=50000),
session: AsyncSession = Depends(get_async_session),
):
"""
Returns all locations within a given radius from a lat/lon point.
Uses PostGIS ST_DWithin (geography).
"""
try:
locations = await LocationService.get_nearby_locations(
session=session,
lat=lat,
lon=lon,
radius= radius
)
return MapQueryResponse(
total=len(locations),
locations=locations
)
except Exception as e:
raise HTTPException(
status_code=500,
detail="Failed to fetch nearby locations"
)

View File

@@ -0,0 +1,93 @@
from fastapi import APIRouter, Depends, HTTPException, Path
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_async_session
from app.services.property_service import PropertyService
from app.schemas.property_schema import PropertyResponse
from app.schemas.full_entry_schema import FullEntryCreateRequest
from app.schemas.property_update_request import PropertyOwnerUpdateRequest
router = APIRouter(prefix="/property", tags=["Property"])
@router.get("/by-location/{location_id}", response_model=PropertyResponse)
async def get_property_by_location(
location_id: int,
session: AsyncSession = Depends(get_async_session),
):
"""
Fetch property + owner using location_id.
Called when agent clicks map box.
"""
property_obj = await PropertyService.get_property_by_location_id(
session=session,
location_id=location_id
)
if not property_obj:
raise HTTPException(
status_code=404,
detail="Property not found for given location"
)
return property_obj
@router.post("/create-full", response_model=PropertyResponse)
async def create_full_entry(
payload: FullEntryCreateRequest,
session: AsyncSession = Depends(get_async_session),
):
"""
Creates Location + Owner + Property in a single transaction.
Frontend cannot send IDs.
"""
try:
property_obj = await PropertyService.create_full_entry(
session=session,
location_data=payload.location,
owner_data=payload.owner,
property_data=payload.property,
)
return property_obj
except Exception as e:
raise HTTPException(
status_code=500,
detail="Failed to create property entry"
)
@router.put("/{property_id}", response_model=PropertyResponse)
async def update_property_and_owner(
property_id: int = Path(..., gt=0),
payload: PropertyOwnerUpdateRequest = ...,
session: AsyncSession = Depends(get_async_session),
):
"""
Update Property + Owner details.
Partial updates supported.
"""
if not payload.owner and not payload.property:
raise HTTPException(
status_code=400,
detail="No update data provided"
)
property_obj = await PropertyService.update_property_and_owner(
session=session,
property_id=property_id,
owner_data=payload.owner.dict(exclude_unset=True) if payload.owner else None,
property_data=payload.property.dict(exclude_unset=True) if payload.property else None,
)
if not property_obj:
raise HTTPException(
status_code=404,
detail="Property not found"
)
return property_obj

View File

@@ -1,35 +0,0 @@
import logging
from fastapi import APIRouter, HTTPException
from app.core.neo4j import get_driver
from app.services.property_service import get_nearest_asset
from app.schemas.location_request import NearestAssetRequest
from app.schemas.property_owner_response import NearestAssetResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/properties", tags=["Geo Search"])
@router.post("/nearest", response_model=NearestAssetResponse)
def nearest_property(payload: NearestAssetRequest):
logger.info(f"Incoming geo request lat={payload.lat}, lng={payload.lng}")
try:
driver = get_driver()
result = get_nearest_asset(driver, payload.lat, payload.lng)
logger.info("Nearest asset query successful")
return result
except ValueError as e:
logger.warning(f"No asset found: {str(e)}")
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.exception("Unexpected server error")
raise HTTPException(
status_code=500,
detail="Internal Server Error"
)

View File

@@ -1,5 +1,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.v1.property_routes import router as property_routes from app.api.v1.endpoints.map import router as map_routes
from app.api.v1.endpoints.property import router as property_routes
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(map_routes)
api_router.include_router(property_routes) api_router.include_router(property_routes)

View File

@@ -1,12 +0,0 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
NEO4J_URI: str
NEO4J_USER: str
NEO4J_PASSWORD: str
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -1,10 +0,0 @@
from neo4j import GraphDatabase
from app.core.config import settings
driver = GraphDatabase.driver(
settings.NEO4J_URI,
auth=(settings.NEO4J_USER, settings.NEO4J_PASSWORD)
)
def get_driver():
return driver

View File

@@ -0,0 +1,83 @@
from datetime import date
from sqlalchemy import (
String,
Integer,
Boolean,
Date,
ForeignKey,
Text
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.models.base import Base
class InsuranceDetails(Base):
__tablename__ = "insurance_details"
id: Mapped[int] = mapped_column(primary_key=True)
# 🔗 Relation with Property
property_id: Mapped[int] = mapped_column(
ForeignKey("properties.id", ondelete="CASCADE"),
index=True
)
# -----------------------------
# REQUIRED FIELDS (Your spec)
# -----------------------------
insurance_company: Mapped[str] = mapped_column(String(150))
claim_number: Mapped[str] = mapped_column(String(100), unique=True)
date_of_loss: Mapped[date] = mapped_column(Date)
adjuster_name: Mapped[str] = mapped_column(String(150), nullable=True)
adjuster_phone: Mapped[str] = mapped_column(String(30), nullable=True)
adjuster_email: Mapped[str] = mapped_column(String(150), nullable=True)
claim_filed: Mapped[bool] = mapped_column(Boolean, default=False)
claim_approved: Mapped[bool] = mapped_column(Boolean, default=False)
# -----------------------------
# 🇺🇸 US Insurance Standard Fields
# -----------------------------
policy_number: Mapped[str] = mapped_column(String(100), nullable=True)
coverage_type: Mapped[str] = mapped_column(
String(50), nullable=True
)
# e.g. Homeowners, Flood, Fire, Liability, Windstorm
claim_type: Mapped[str] = mapped_column(
String(50), nullable=True
)
# e.g. Roof Damage, Water Damage, Fire, Storm, Theft
deductible_amount: Mapped[int] = mapped_column(nullable=True)
claim_amount: Mapped[int] = mapped_column(nullable=True)
approved_amount: Mapped[int] = mapped_column(nullable=True)
payment_status: Mapped[str] = mapped_column(
String(50), nullable=True
)
# Pending / Paid / Denied / Under Review
date_claim_filed: Mapped[date] = mapped_column(Date, nullable=True)
date_claim_closed: Mapped[date] = mapped_column(Date, nullable=True)
insurance_agent_name: Mapped[str] = mapped_column(String(150), nullable=True)
insurance_agent_phone: Mapped[str] = mapped_column(String(30), nullable=True)
notes: Mapped[str] = mapped_column(Text, nullable=True)
# -----------------------------
# Relationship
# -----------------------------
property = relationship("Property", back_populates="insurance_records")

View File

@@ -1,4 +1,5 @@
from sqlalchemy import String, Integer, Float, BigInteger from geoalchemy2 import Geography
from sqlalchemy import String, Integer, Float, BigInteger, UniqueConstraint
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.db.models.base import Base from app.db.models.base import Base
@@ -7,6 +8,9 @@ from app.db.models.base import Base
class Location(Base): class Location(Base):
__tablename__ = "locations" __tablename__ = "locations"
__table_args__ = (
UniqueConstraint("osm_type", "osm_id", name="uq_osm_location"),
)
id: Mapped[int] = mapped_column(primary_key=True, index=True) id: Mapped[int] = mapped_column(primary_key=True, index=True)
@@ -38,6 +42,10 @@ class Location(Base):
bbox_lon_min: Mapped[float] = mapped_column(Float) bbox_lon_min: Mapped[float] = mapped_column(Float)
bbox_lon_max: Mapped[float] = mapped_column(Float) bbox_lon_max: Mapped[float] = mapped_column(Float)
geom: Mapped[str] = mapped_column(
Geography("POINT", srid=4326)
)
property = relationship( property = relationship(
"Property", "Property",
back_populates="location", back_populates="location",

View File

@@ -63,3 +63,9 @@ class Property(Base):
back_populates="property", back_populates="property",
uselist=False, uselist=False,
) )
insurance_records = relationship(
"InsuranceDetails",
back_populates="property",
cascade="all, delete-orphan"
)

View File

@@ -8,6 +8,6 @@ AsyncSessionLocal = async_sessionmaker(
) )
async def get_db(): async def get_async_session():
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
yield session yield session

View File

@@ -0,0 +1,50 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models.location import Location
from geoalchemy2.shape import from_shape
from shapely.geometry import Point
from geoalchemy2.functions import ST_DWithin
class LocationRepository:
@staticmethod
async def get_by_osm(session: AsyncSession, osm_type: str, osm_id: int):
stmt = select(Location).where(
Location.osm_type == osm_type,
Location.osm_id == osm_id
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def create(session, data):
# For geography this is still correct
geom = from_shape(Point(data["longitude"], data["latitude"]), srid=4326)
location = Location(
**data,
geom=geom
)
session.add(location)
await session.flush()
return location
@staticmethod
async def find_within_radius(session, lat, lon, radius_m=2000):
# Create a geography point instead of string
point = from_shape(Point(lon, lat), srid=4326)
stmt = select(Location).where(
ST_DWithin(
Location.geom,
point,
radius_m
)
)
result = await session.execute(stmt)
return result.scalars().all()

View File

@@ -0,0 +1,8 @@
class OwnerRepository:
@staticmethod
async def update(session, owner, data: dict):
for key, value in data.items():
setattr(owner, key, value)
await session.flush()
return owner

View File

@@ -1,110 +1,20 @@
from neo4j import Driver from sqlalchemy import select
from sqlalchemy.orm import selectinload
QUERY_NEAREST = """ from app.db.models.property import Property
WITH point({latitude: $lat, longitude: $lng}) AS userLocation
MATCH (o:Owner)-[:OWNS]->(p:Property)-[:LOCATED_AT]->(l:Location)
WHERE l.location IS NOT NULL
WITH o, p,
point.distance(l.location, userLocation) AS distanceMeters
ORDER BY distanceMeters ASC
LIMIT 1
RETURN
// Owner Projection (API Safe)
{
owner_id: o.owner_id,
full_name: o.full_name,
email: o.email,
phone: o.phone,
entity_type: o.entity_type,
mailing_address: o.mailing_address,
credit_score_est: o.credit_score_est,
income_bracket: o.income_bracket,
net_worth_est: o.net_worth_est,
portfolio_size: o.portfolio_size,
min_price_expectation: o.min_price_expectation,
preferred_close_days: o.preferred_close_days,
urgency_score: o.urgency_score,
is_absentee: o.is_absentee,
willing_to_sell: o.willing_to_sell,
willing_to_partner: o.willing_to_partner
} AS owner,
// Property Projection (API Safe)
{
property_id: p.property_id,
internal_asset_code: p.internal_asset_code,
structure_type: p.structure_type,
listing_status: p.listing_status,
occupancy_status: p.occupancy_status,
property_grade: p.property_grade,
energy_rating: p.energy_rating,
year_built: p.year_built,
floors_count: p.floors_count,
bedrooms: p.bedrooms,
bathrooms: p.bathrooms,
total_built_sqft: p.total_built_sqft,
lot_size_sqft: p.lot_size_sqft,
garage_spaces: p.garage_spaces,
purchase_price: p.purchase_price,
expected_sale_price: p.expected_sale_price,
market_value_est: p.market_value_est,
current_rent: p.current_rent,
rental_yield_percent: p.rental_yield_percent,
vacancy_days: p.vacancy_days,
tenant_present: p.tenant_present,
exterior_condition: p.exterior_condition,
foundation_type: p.foundation_type,
roof_type: p.roof_type,
roof_material: p.roof_material,
roof_condition: p.roof_condition,
roof_pitch: p.roof_pitch,
roof_age_years: p.roof_age_years,
siding_material: p.siding_material,
gutter_status: p.gutter_status,
hvac_type: p.hvac_type,
electric_type: p.electric_type,
plumbing_type: p.plumbing_type,
solar_installed: p.solar_installed,
mold_risk_level: p.mold_risk_level,
termite_risk_level: p.termite_risk_level,
structural_risk_level: p.structural_risk_level,
fire_damage_flag: p.fire_damage_flag,
water_damage_flag: p.water_damage_flag,
created_at: toString(p.created_at),
updated_at: toString(p.updated_at)
} AS property,
distanceMeters
"""
def find_nearest_property_and_owner(driver: Driver, lat: float, lng: float): class PropertyRepository:
@staticmethod
async def get_by_location_id(session, location_id: int):
with driver.session(database="neo4j") as session: stmt = (
result = session.run( select(Property)
QUERY_NEAREST, .options(
lat=lat, selectinload(Property.owner),
lng=lng selectinload(Property.location),
)
.where(Property.location_id == location_id)
) )
record = result.single() result = await session.execute(stmt)
return result.scalar_one_or_none()
if not record:
return None
return {
"owner": record["owner"],
"property": record["property"],
"distanceMeters": record["distanceMeters"]
}

View File

@@ -0,0 +1,10 @@
from pydantic import BaseModel
from app.schemas.location_create_schema import LocationCreate
from app.schemas.owner_create_schema import OwnerCreate
from app.schemas.property_create_schema import PropertyCreate
class FullEntryCreateRequest(BaseModel):
location: LocationCreate
owner: OwnerCreate
property: PropertyCreate

View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel
class LocationCreate(BaseModel):
place_id: int
osm_id: int
osm_type: str
latitude: float
longitude: float
house_number: str | None = None
road: str
city: str
county: str
state: str
postcode: str
country: str
country_code: str
display_name: str
bbox_lat_min: float
bbox_lat_max: float
bbox_lon_min: float
bbox_lon_max: float

View File

@@ -1,6 +0,0 @@
from pydantic import BaseModel, Field
class NearestAssetRequest(BaseModel):
lat: float = Field(..., ge=-90, le=90, example=34.01544)
lng: float = Field(..., ge=-180, le=180, example=-118.2201)

49
app/schemas/map_schema.py Normal file
View File

@@ -0,0 +1,49 @@
from pydantic import BaseModel, Field
class MapQueryParams(BaseModel):
lat: float = Field(..., ge=-90, le=90, description="Latitude")
lon: float = Field(..., ge=-180, le=180, description="Longitude")
radius: int = Field(2000, gt=0, le=50000, description="Search radius in meters")
class LocationResponse(BaseModel):
id: int
# OSM identifiers
place_id: int
osm_id: int
osm_type: str
# Coordinates
latitude: float
longitude: float
# Address info
house_number: str | None = None
road: str
city: str
county: str
state: str
postcode: str
country: str
country_code: str
# Display name
display_name: str
# Bounding box
bbox_lat_min: float
bbox_lat_max: float
bbox_lon_min: float
bbox_lon_max: float
class Config:
from_attributes = True
class MapQueryResponse(BaseModel):
total: int
locations: list[LocationResponse]

View File

@@ -0,0 +1,15 @@
from pydantic import BaseModel
class OwnerCreate(BaseModel):
full_name: str
phone_number: str
email: str
occupation: str
annual_income_range: str
willing_to_rent: bool = False
desired_rent_price: int | None = None
willing_to_sell: bool = False
desired_sell_price: int | None = None

View File

@@ -0,0 +1,17 @@
from pydantic import BaseModel
class OwnerResponse(BaseModel):
id: int
full_name: str
phone_number: str
email: str
occupation: str
annual_income_range: str
willing_to_rent: bool
desired_rent_price: int | None
willing_to_sell: bool
desired_sell_price: int | None
class Config:
from_attributes = True

View File

@@ -0,0 +1,15 @@
from pydantic import BaseModel
class OwnerUpdate(BaseModel):
full_name: str | None = None
phone_number: str | None = None
email: str | None = None
occupation: str | None = None
annual_income_range: str | None = None
willing_to_rent: bool | None = None
desired_rent_price: int | None = None
willing_to_sell: bool | None = None
desired_sell_price: int | None = None

View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel
from datetime import date
class PropertyCreate(BaseModel):
property_type: str
built_up_area: float
bedrooms: int
bathrooms: int
purchase_date: date
purchase_price: float
sale_listing_date: date | None = None
sale_asking_price: float | None = None
last_renovation_date: date | None = None
renovation_description: str | None = None
roof_repair_date: date | None = None
roof_condition: str
available_for_rent: bool = False
expected_rent: float | None = None
rental_available_date: date | None = None

View File

@@ -1,172 +0,0 @@
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
# -----------------------------
# Owner Response Schema
# -----------------------------
class OwnerResponse(BaseModel):
owner_id: str = Field(..., example="OWN_9001")
full_name: str = Field(..., example="Michael Anderson")
email: Optional[str] = Field(None, example="michael.anderson@email.com")
phone: Optional[str] = Field(None, example="+14155552671")
entity_type: str = Field(..., example="Individual")
mailing_address: Optional[str] = Field(
None,
example="742 Evergreen Terrace, San Diego, CA 92101"
)
credit_score_est: Optional[int] = Field(None, ge=300, le=850, example=780)
income_bracket: Optional[str] = Field(None, example="$100k+")
net_worth_est: Optional[int] = Field(None, example=1250000)
portfolio_size: Optional[int] = Field(None, example=3)
min_price_expectation: Optional[int] = Field(None, example=650000)
preferred_close_days: Optional[int] = Field(None, example=30)
urgency_score: Optional[int] = Field(None, ge=0, le=100, example=72)
is_absentee: Optional[bool] = Field(None, example=True)
willing_to_sell: Optional[bool] = Field(None, example=True)
willing_to_partner: Optional[bool] = Field(None, example=False)
# ⚠ Recommended: Do not expose in public APIs
tax_id_hash: Optional[str] = Field(
None,
example="HASHX9921",
description="Hashed tax identifier (masked)"
)
class Config:
from_attributes = True
# -----------------------------
# Property Response Schema
# -----------------------------
class PropertyResponse(BaseModel):
property_id: str = Field(..., example="PROP_1001")
internal_asset_code: str = Field(..., example="ASSET-LA-7782")
structure_type: str = Field(..., example="SingleFamily")
listing_status: str = Field(..., example="ForSale")
occupancy_status: str = Field(..., example="OwnerOccupied")
property_grade: str = Field(..., example="A")
energy_rating: Optional[str] = Field(None, example="B+")
year_built: int = Field(..., example=2006)
floors_count: int = Field(..., example=2)
bedrooms: int = Field(..., example=4)
bathrooms: int = Field(..., example=3)
total_built_sqft: int = Field(..., example=2480)
lot_size_sqft: Optional[int] = Field(None, example=6200)
garage_spaces: Optional[int] = Field(None, example=2)
purchase_price: Optional[int] = Field(None, example=590000)
expected_sale_price: Optional[int] = Field(None, example=740000)
market_value_est: Optional[int] = Field(None, example=685000)
current_rent: Optional[int] = Field(None, example=0)
rental_yield_percent: Optional[float] = Field(None, example=6.2)
vacancy_days: Optional[int] = Field(None, example=0)
tenant_present: Optional[bool] = Field(None, example=False)
# Structural / Condition
exterior_condition: Optional[str] = Field(None, example="Good")
foundation_type: Optional[str] = Field(None, example="Slab")
roof_type: Optional[str] = Field(None, example="Gable")
roof_material: Optional[str] = Field(None, example="Asphalt Shingle")
roof_condition: Optional[str] = Field(None, example="Good")
roof_pitch: Optional[str] = Field(None, example="Medium")
roof_age_years: Optional[int] = Field(None, example=7)
siding_material: Optional[str] = Field(None, example="Hardie Board")
gutter_status: Optional[str] = Field(None, example="Functional")
hvac_type: Optional[str] = Field(None, example="Central")
electric_type: Optional[str] = Field(None, example="Copper")
plumbing_type: Optional[str] = Field(None, example="PEX")
solar_installed: Optional[bool] = Field(None, example=False)
# Risk Indicators
mold_risk_level: Optional[str] = Field(None, example="Low")
termite_risk_level: Optional[str] = Field(None, example="Low")
structural_risk_level: Optional[str] = Field(None, example="Low")
fire_damage_flag: Optional[bool] = Field(None, example=False)
water_damage_flag: Optional[bool] = Field(None, example=False)
# Timestamps
created_at: Optional[datetime] = Field(
None,
example="2026-01-29T16:43:44.285Z"
)
updated_at: Optional[datetime] = Field(
None,
example="2026-01-29T16:43:44.285Z"
)
class Config:
from_attributes = True
# -----------------------------
# Final Nearest Asset Response
# -----------------------------
class NearestAssetResponse(BaseModel):
owner: OwnerResponse
property: PropertyResponse
distanceMeters: float = Field(..., example=342.55)

View File

@@ -0,0 +1,33 @@
from pydantic import BaseModel
from datetime import date
from app.schemas.owner_schema import OwnerResponse
class PropertyResponse(BaseModel):
id: int
property_type: str
built_up_area: float
bedrooms: int
bathrooms: int
purchase_date: date
purchase_price: float
sale_listing_date: date | None
sale_asking_price: float | None
last_renovation_date: date | None
renovation_description: str | None
roof_repair_date: date | None
roof_condition: str
available_for_rent: bool
expected_rent: float | None
rental_available_date: date | None
owner: OwnerResponse
class Config:
from_attributes = True

View File

@@ -0,0 +1,8 @@
from pydantic import BaseModel
from app.schemas.owner_update_schema import OwnerUpdate
from app.schemas.property_update_schema import PropertyUpdate
class PropertyOwnerUpdateRequest(BaseModel):
owner: OwnerUpdate | None = None
property: PropertyUpdate | None = None

View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel
from datetime import date
class PropertyUpdate(BaseModel):
property_type: str | None = None
built_up_area: float | None = None
bedrooms: int | None = None
bathrooms: int | None = None
purchase_date: date | None = None
purchase_price: float | None = None
sale_listing_date: date | None = None
sale_asking_price: float | None = None
last_renovation_date: date | None = None
renovation_description: str | None = None
roof_repair_date: date | None = None
roof_condition: str | None = None
available_for_rent: bool | None = None
expected_rent: float | None = None
rental_available_date: date | None = None

View File

@@ -0,0 +1,14 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.repositories.location_repo import LocationRepository
class LocationService:
@staticmethod
async def get_nearby_locations(session: AsyncSession, lat: float, lon: float, radius: int = 2000):
return await LocationRepository.find_within_radius(
session=session,
lat=lat,
lon=lon,
radius_m=radius
)

View File

@@ -1,10 +1,102 @@
from app.repositories.property_repo import find_nearest_property_and_owner from app.repositories.location_repo import LocationRepository
from app.repositories.property_repo import PropertyRepository
from app.db.models.owner import Owner
from app.db.models.property import Property
def get_nearest_asset(driver, lat: float, lng: float):
data = find_nearest_property_and_owner(driver, lat, lng) class PropertyService:
@staticmethod
async def get_property_by_osm(session, osm_type, osm_id):
location = await LocationRepository.get_by_osm(
session, osm_type, osm_id
)
if not location:
return None
property = await PropertyRepository.get_by_location_id(
session, location.id
)
return property
@staticmethod
async def create_full_entry(session, location_data, owner_data, property_data):
async with session.begin():
# Check existing location
location = await LocationRepository.get_by_osm(
session,
location_data.osm_type,
location_data.osm_id
)
if not location:
location = await LocationRepository.create(
session,
location_data.dict()
)
# Check if property already exists for this location
existing_property = await PropertyRepository.get_by_location_id(
session,
location.id
)
if existing_property:
# return existing instead of crashing
return existing_property
# Create owner
owner = Owner(**owner_data.dict())
session.add(owner)
await session.flush()
# Create property
prop_data = property_data.dict()
prop_data["owner_id"] = owner.id
prop_data["location_id"] = location.id
property_obj = Property(**prop_data)
session.add(property_obj)
return property_obj
@staticmethod
async def update_property_and_owner(session, property_id, owner_data, property_data):
async with session.begin():
property_obj = await session.get(Property, property_id)
if not property_obj:
return None
owner = property_obj.owner
# Update owner
if owner_data:
for k, v in owner_data.items():
setattr(owner, k, v)
# Update property
if property_data:
for k, v in property_data.items():
setattr(property_obj, k, v)
return property_obj
@staticmethod
async def get_property_by_location_id(session, location_id: int):
property_obj = await PropertyRepository.get_by_location_id(
session, location_id
)
return property_obj
if not data:
raise ValueError("No nearby property found")
return data

View File

@@ -8,10 +8,12 @@ dependencies = [
"alembic>=1.18.3", "alembic>=1.18.3",
"asyncpg>=0.31.0", "asyncpg>=0.31.0",
"fastapi>=0.128.0", "fastapi>=0.128.0",
"geoalchemy2>=0.18.1",
"neo4j>=6.1.0", "neo4j>=6.1.0",
"pydantic>=2.12.5", "pydantic>=2.12.5",
"pydantic-settings>=2.12.0", "pydantic-settings>=2.12.0",
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
"shapely>=2.1.2",
"sqlalchemy>=2.0.46", "sqlalchemy>=2.0.46",
"uvicorn>=0.40.0", "uvicorn>=0.40.0",
] ]

0
test.py Normal file
View File

119
uv.lock generated
View File

@@ -107,10 +107,12 @@ dependencies = [
{ name = "alembic" }, { name = "alembic" },
{ name = "asyncpg" }, { name = "asyncpg" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "geoalchemy2" },
{ name = "neo4j" }, { name = "neo4j" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "shapely" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
@@ -120,10 +122,12 @@ requires-dist = [
{ name = "alembic", specifier = ">=1.18.3" }, { name = "alembic", specifier = ">=1.18.3" },
{ name = "asyncpg", specifier = ">=0.31.0" }, { name = "asyncpg", specifier = ">=0.31.0" },
{ name = "fastapi", specifier = ">=0.128.0" }, { name = "fastapi", specifier = ">=0.128.0" },
{ name = "geoalchemy2", specifier = ">=0.18.1" },
{ name = "neo4j", specifier = ">=6.1.0" }, { name = "neo4j", specifier = ">=6.1.0" },
{ name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic", specifier = ">=2.12.5" },
{ name = "pydantic-settings", specifier = ">=2.12.0" }, { name = "pydantic-settings", specifier = ">=2.12.0" },
{ name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "shapely", specifier = ">=2.1.2" },
{ name = "sqlalchemy", specifier = ">=2.0.46" }, { name = "sqlalchemy", specifier = ">=2.0.46" },
{ name = "uvicorn", specifier = ">=0.40.0" }, { name = "uvicorn", specifier = ">=0.40.0" },
] ]
@@ -143,6 +147,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
] ]
[[package]]
name = "geoalchemy2"
version = "0.18.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "sqlalchemy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/df/f6d689120a15a2287794e16696c3bdb4cf2e53038255d288b61a4d59e1fa/geoalchemy2-0.18.1.tar.gz", hash = "sha256:4bdc7daf659e36f6456e2f2c3bcce222b879584921a4f50a803ab05fa2bb3124", size = 239302, upload-time = "2025-11-18T15:12:05.296Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/25/b3d6fc757d8d909e0e666ec6fbf1b7914e9ad18d6e1b08994cd9d2e63330/geoalchemy2-0.18.1-py3-none-any.whl", hash = "sha256:a49d9559bf7acbb69129a01c6e1861657c15db420886ad0a09b1871fb0ff4bdb", size = 81261, upload-time = "2025-11-18T15:12:03.985Z" },
]
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.3.1" version = "3.3.1"
@@ -271,6 +288,65 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/5c/ee71e2dd955045425ef44283f40ba1da67673cf06404916ca2950ac0cd39/neo4j-6.1.0-py3-none-any.whl", hash = "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d", size = 325326, upload-time = "2026-01-12T11:27:33.196Z" }, { url = "https://files.pythonhosted.org/packages/70/5c/ee71e2dd955045425ef44283f40ba1da67673cf06404916ca2950ac0cd39/neo4j-6.1.0-py3-none-any.whl", hash = "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d", size = 325326, upload-time = "2026-01-12T11:27:33.196Z" },
] ]
[[package]]
name = "numpy"
version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" },
{ url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" },
{ url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" },
{ url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" },
{ url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" },
{ url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" },
{ url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" },
{ url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" },
{ url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" },
{ url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" },
{ url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" },
{ url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" },
{ url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" },
{ url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" },
{ url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" },
{ url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" },
{ url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" },
{ url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" },
{ url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" },
{ url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" },
{ url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" },
{ url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" },
{ url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" },
{ url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" },
{ url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" },
{ url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" },
{ url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" },
{ url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" },
{ url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" },
{ url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" },
{ url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" },
{ url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" },
{ url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" },
{ url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" },
{ url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" },
{ url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" },
{ url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" },
{ url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" },
{ url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" },
{ url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"
@@ -371,6 +447,49 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
] ]
[[package]]
name = "shapely"
version = "2.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" },
{ url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" },
{ url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" },
{ url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" },
{ url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" },
{ url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" },
{ url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" },
{ url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" },
{ url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" },
{ url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" },
{ url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" },
{ url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" },
{ url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" },
{ url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" },
{ url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" },
{ url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" },
{ url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" },
{ url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" },
{ url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" },
{ url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" },
{ url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" },
{ url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" },
{ url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" },
{ url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" },
{ url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" },
{ url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" },
{ url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" },
{ url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" },
{ url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" },
]
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.46" version = "2.0.46"