Metadata-Version: 2.4
Name: htag-sdk
Version: 1.4.0
Summary: Official Python SDK for the HtAG Location Intelligence API — address search, property data, and market analytics for Australia
Project-URL: Homepage, https://developer.htagai.com
Project-URL: Documentation, https://developer.htagai.com
Project-URL: Repository, https://github.com/HtaG-Analytics/htag-sdk-python
Project-URL: Issues, https://github.com/HtaG-Analytics/htag-sdk-python/issues
Author-email: Sasa Savic <sasa.savic@htag.com.au>
License-Expression: MIT
Keywords: address,api,australia,htag,location-intelligence,market-data,property,real-estate,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Description-Content-Type: text/markdown

# htag-sdk

The official Python SDK for the [HtAG](https://htagai.com) Location Intelligence API.

Provides typed, ergonomic access to Australian address data, property valuations, sales records, and market analytics with both synchronous and asynchronous clients.

```python
from htag_sdk import HtAgApi

client = HtAgApi(api_key="sk-...", environment="prod")

results = client.address.geocode("100 George St Sydney")
for r in results.results:
    print(f"{r.address_label}  ({r.address_key})")
```

## Installation

```bash
pip install htag-sdk
```

Or with your preferred package manager:

```bash
uv add htag-sdk
poetry add htag-sdk
```

Requires Python 3.9+.

## Quick Start

### 1. Get an API Key

Sign up at [developer.htagai.com](https://developer.htagai.com) and create an API key from the Settings page.

### 2. Create a Client

```python
from htag_sdk import HtAgApi

client = HtAgApi(
    api_key="sk-org--your-org-id-your-key-value",
    environment="prod",   # "dev" or "prod"
)
```

Or use a custom base URL:

```python
client = HtAgApi(api_key="sk-...", base_url="https://api.staging.htagai.com")
```

### 3. Make Requests

```python
# Geocode an address
results = client.address.geocode("15 Miranda Court Noble Park")
print(results.total, "matches")

# Get property estimates
est = client.property.estimates(address_key="15MIRANDACOURTNOBLEPARKvic3174")
for record in est.results:
    print(f"Price estimate: ${record.price_estimate:,}")
    print(f"Last sold: ${record.last_sold_price:,} on {record.last_sold_date}")

# Close when done (or use a context manager)
client.close()
```

## Usage

### Address Geocode

Resolve a free-text address to structured location data with geographic identifiers.

```python
results = client.address.geocode(
    "100 Hickox St Traralgon",
    limit=5,         # max results (1 - 50)
)

for match in results.results:
    print(f"{match.address_label}")
    print(f"  Key: {match.address_key}")
    print(f"  Location: {match.lat}, {match.lon}")
```

### Address Standardisation

Standardise raw address strings into structured, canonical components.

```python
result = client.address.standardise([
    "12 / 100-102 HICKOX STR TRARALGON, VIC 3844",
    "15a smith st fitzroy vic 3065",
])

for item in result.results:
    if item.error:
        print(f"Failed: {item.input_address} -- {item.error}")
    else:
        addr = item.standardised_address
        print(f"{item.input_address}")
        print(f"  -> {addr.street_number} {addr.street_name} {addr.street_type}")
        print(f"     {addr.suburb_or_locality} {addr.state} {addr.postcode}")
        print(f"  Key: {item.address_key}")
```

### Address Environment

Retrieve environmental risk data for an address including flood, bushfire, heritage, and zoning.

```python
env = client.address.environment(address="15 Miranda Court, Noble Park VIC 3174")
for record in env.results:
    print(f"Bushfire: {record.bushfire}, Flood: {record.flood}")
    print(f"Heritage: {record.heritage}, Zoning: {record.zoning}")
```

### Address Demographics

Retrieve socio-economic indices (SEIFA) and housing tenure data.

```python
demo = client.address.demographics(address="15 Miranda Court, Noble Park VIC 3174")
for record in demo.results:
    print(f"IRSAD: {record.IRSAD}, IER: {record.IER}")
```

### Property Summary

Retrieve physical property attributes for an address.

```python
summary = client.property.summary(address_key="100102HICKOXSTREETTRARALGONVIC3844")
for record in summary.results:
    print(f"Type: {record.property_type}")
    print(f"Beds: {record.beds}, Baths: {record.baths}, Parking: {record.parking}")
    print(f"Land: {record.lot_size} sqm, Floor: {record.floor_area} sqm")
```

### Property Estimates

Retrieve valuation estimates and transaction history for an address.

```python
est = client.property.estimates(address_key="100102HICKOXSTREETTRARALGONVIC3844")
for record in est.results:
    print(f"Price estimate: ${record.price_estimate:,}")
    print(f"Rent estimate: ${record.rent_estimate}/wk")
    print(f"Last sold: ${record.last_sold_price:,} on {record.last_sold_date}")
```

### Property Market

Retrieve market position indicators for an address.

```python
mkt = client.property.market(address_key="100102HICKOXSTREETTRARALGONVIC3844")
for record in mkt.results:
    print(f"Rental %: {record.rental_percentage:.0%}")
    print(f"Years to own: {record.years_to_own}")
    print(f"Hold period: {record.hold_period} years")
```

### Sold Property Search

Search for recently sold properties near an address or coordinates.

```python
sold = client.property.sold_search(
    address="100 George St, Sydney NSW 2000",
    radius=2000,              # metres
    property_type="house",
    sale_value_min=500_000,
    sale_value_max=2_000_000,
    bedrooms_min=3,
    start_date="2024-01-01",
)

print(f"{sold.total} properties found")
for prop in sold.results:
    price = f"${prop.sold_price:,.0f}" if prop.sold_price else "undisclosed"
    print(f"  {prop.street_address}, {prop.suburb} -- {price} ({prop.sold_date})")
```

All filter parameters are optional:

| Parameter | Type | Description |
|-----------|------|-------------|
| `address` | str | Free-text address to centre the search on |
| `address_key` | str | GNAF address key |
| `lat`, `lon` | float | Coordinates for point-based search |
| `radius` | int | Search radius in metres (default 2000, max 5000) |
| `proximity` | str | `"any"`, `"sameStreet"`, or `"sameSuburb"` |
| `property_type` | str | `"house"`, `"unit"`, `"townhouse"`, `"land"`, `"rural"` |
| `sale_value_min`, `sale_value_max` | float | Price range filter (AUD) |
| `bedrooms_min`, `bedrooms_max` | int | Bedroom count range |
| `bathrooms_min`, `bathrooms_max` | int | Bathroom count range |
| `car_spaces_min`, `car_spaces_max` | int | Car space range |
| `start_date`, `end_date` | str | Date range (ISO 8601, e.g. `"2024-01-01"`) |
| `land_area_min`, `land_area_max` | int | Land area in sqm |

### Market Summary

Get headline market metrics at suburb or LGA level.

```python
summary = client.markets.summary(
    level="suburb",
    area_id=["SAL10001"],
    property_type=["house"],
)

for record in summary.results:
    print(f"{record.suburb} ({record.state_name})")
    print(f"  Typical price: ${record.typical_price:,}")
    print(f"  Rent: ${record.rent}/wk")
```

### Market Growth

Retrieve cumulative or annualised growth rates for price, rent, and yield.

```python
growth = client.markets.growth_cumulative(
    level="suburb",
    area_id=["SAL10001"],
    property_type=["house"],
)

for record in growth.results:
    print(f"1Y price growth: {record.one_y_price_growth:.1%}")
    print(f"5Y price growth: {record.five_y_price_growth:.1%}")
```

### Market Trends

Access historical trend data via `client.markets.trends`. All trend methods share the same parameter signature:

```python
# Price history
prices = client.markets.trends.price(
    level="suburb",
    area_id=["SAL10001"],
    property_type=["house"],
    period_end_min="2020-01-01",
    limit=50,
)
for p in prices.results:
    print(f"{p.period_end}: ${p.typical_price:,} ({p.sales} sales)")

# Rent history
rents = client.markets.trends.rent(level="suburb", area_id=["SAL10001"])

# Yield history
yields = client.markets.trends.yield_history(level="suburb", area_id=["SAL10001"])

# Search interest index (buy/rent search indices)
search = client.markets.trends.search_index(level="suburb", area_id=["SAL10001"])

# Hold period
hold = client.markets.trends.hold_period(level="suburb", area_id=["SAL10001"])

# Growth rates (price, rent, yield changes)
growth = client.markets.trends.growth_rates(level="suburb", area_id=["SAL10001"])

# Demand profile (sales by dwelling type and bedrooms)
demand = client.markets.trends.demand_profile(level="suburb", area_id=["SAL10001"])

# Stock on market
som = client.markets.trends.stock_on_market(level="suburb", area_id=["SAL10001"])

# Days on market
dom = client.markets.trends.days_on_market(level="suburb", area_id=["SAL10001"])

# Clearance rate
cr = client.markets.trends.clearance_rate(level="suburb", area_id=["SAL10001"])

# Vacancy rate
vac = client.markets.trends.vacancy(level="suburb", area_id=["SAL10001"])
```

Common trend parameters:

| Parameter | Type | Description |
|-----------|------|-------------|
| `level` | str | `"suburb"` or `"lga"` (required) |
| `area_id` | list[str] | Area identifiers (required) |
| `property_type` | list[str] | `["house"]`, `["unit"]`, etc. |
| `period_end_min` | str | Filter from this date |
| `period_end_max` | str | Filter up to this date |
| `bedrooms` | str or list[str] | Bedroom filter |
| `limit` | int | Max results (default 100, max 1000) |
| `offset` | int | Pagination offset |

## Internal API

Some endpoints require the `internal_api` scope on your API key. These are accessed via the `client.internal` namespace:

```python
# Address search (trigram similarity matching)
results = client.internal.address.search("100 George St Sydney")

# Address insights (enriched address data)
insights = client.internal.address.insights(
    address="15 Miranda Court, Noble Park VIC 3174"
)

# Automated Valuation Model (batch, up to 50 properties)
avm = client.internal.property.avm(
    address_key=["100102HICKOXSTREETTRARALGONVIC3844"]
)

# Market snapshots with filtering
snapshots = client.internal.markets.snapshots(
    level="suburb",
    property_type=["house"],
    area_id=["SAL10001"],
)

# Advanced market query with logical filters
results = client.internal.markets.query({
    "level": "suburb",
    "mode": "search",
    "property_types": ["house"],
    "typical_price_min": 500_000,
    "logic": {
        "and": [
            {"field": "one_y_price_growth", "gte": 0.05},
            {"field": "vacancy_rate", "lte": 0.03},
        ]
    },
})

# Internal trend endpoints
supply = client.internal.markets.trends.supply_demand(
    level="suburb", area_id=["SAL10001"]
)
perf = client.internal.markets.trends.performance(
    level="suburb", area_id=["SAL10001"]
)
```

If you call an internal method without the required scope, the API will return a 403 error.

## Async Usage

Every method is available as an async equivalent:

```python
import asyncio
from htag_sdk import AsyncHtAgApi

async def main():
    client = AsyncHtAgApi(api_key="sk-...", environment="prod")

    # All the same methods, just with await
    results = await client.address.geocode("100 George St Sydney")
    est = await client.property.estimates(address_key="...")
    sold = await client.property.sold_search(address="100 George St Sydney")
    prices = await client.markets.trends.price(level="suburb", area_id=["SAL10001"])

    # Internal methods also available
    insights = await client.internal.address.insights(address="100 George St Sydney")

    await client.close()

asyncio.run(main())
```

### Context Manager

Both clients support context managers for automatic cleanup:

```python
# Sync
with HtAgApi(api_key="sk-...") as client:
    results = client.address.geocode("Sydney")

# Async
async with AsyncHtAgApi(api_key="sk-...") as client:
    results = await client.address.geocode("Sydney")
```

## Error Handling

The SDK raises typed exceptions for API errors:

```python
from htag_sdk import (
    HtAgApi,
    AuthenticationError,
    RateLimitError,
    ValidationError,
    ServerError,
    ConnectionError,
)

client = HtAgApi(api_key="sk-...")

try:
    results = client.address.geocode("Syd")
except AuthenticationError as e:
    # 401 or 403 -- bad API key or insufficient scope
    print(f"Auth failed: {e.message}")
except RateLimitError as e:
    # 429 -- throttled (after exhausting retries)
    print(f"Rate limited. Retry after: {e.retry_after}s")
except ValidationError as e:
    # 400 or 422 -- bad request params
    print(f"Invalid request: {e.message}")
    print(f"Details: {e.body}")
except ServerError as e:
    # 5xx -- upstream failure (after exhausting retries)
    print(f"Server error: {e.status_code}")
except ConnectionError as e:
    # Network/DNS/TLS failure
    print(f"Connection failed: {e.message}")
```

All exceptions carry:
- `message` -- human-readable description
- `status_code` -- HTTP status (if applicable)
- `body` -- raw response body
- `request_id` -- request identifier (if returned by the API)

## Retries

The SDK automatically retries transient failures:

- **Retried statuses**: 429, 500, 502, 503, 504
- **Max retries**: 3 (configurable)
- **Backoff**: exponential (0.5s base, 2x multiplier, 25% jitter, 30s cap)
- **429 handling**: respects `Retry-After` header

Configure retry behaviour:

```python
client = HtAgApi(
    api_key="sk-...",
    max_retries=5,    # default is 3
    timeout=120.0,    # request timeout in seconds (default 60)
)
```

## Configuration Reference

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_key` | str | required | Your HtAG API key |
| `environment` | str | `"prod"` | `"dev"` or `"prod"` |
| `base_url` | str | -- | Custom base URL (overrides environment) |
| `timeout` | float | `60.0` | Request timeout in seconds |
| `max_retries` | int | `3` | Maximum retry attempts |

## Requirements

- Python >= 3.9
- [httpx](https://www.python-httpx.org/) >= 0.27
- [Pydantic](https://docs.pydantic.dev/) >= 2.0

## License

MIT
