Metadata-Version: 2.4
Name: propertiesapi
Version: 0.2.0
Summary: Official Python client for PropertiesAPI: Zillow property and listing data as JSON from a Zillow link, zpid or address.
Project-URL: Homepage, https://propertiesapi.com
Project-URL: Documentation, https://propertiesapi.com/docs/sdks
Project-URL: API reference, https://propertiesapi.com/docs
Project-URL: Support, mailto:support@propertiesapi.com
Author-email: PropertiesAPI <support@propertiesapi.com>
Maintainer-email: PropertiesAPI <support@propertiesapi.com>
License-Expression: MIT
License-File: LICENSE
Keywords: propertiesapi,property-data,real-estate,sdk,zestimate,zillow,zillow-api
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# propertiesapi (Python)

The official Python client for [PropertiesAPI](https://propertiesapi.com). Send a Zillow link, a zpid or an address
and get the Zillow property and listing data back as JSON: listing price and status, Zestimate, rent Zestimate,
beds, baths, living area, photos, price history, tax history, schools, broker and agent.

- Typed (`py.typed`, TypedDicts for the response, `data["calculated"]` and `meta["completeness"]` included), Python 3.8+.
- No dependencies: it uses only the standard library.
- Typed errors for every API error code (batch items included), automatic retries of the errors that come with a
  `retry-after` wait (rate limits, busy, the daily caps), timeouts.
- Your credits left after every call and every error, prepaid credits included, from the `X-Credits-*` headers.

PropertiesAPI is an independent service and is not affiliated with, endorsed by or sponsored by Zillow.

## Install

Install the wheel from our site (Python 3.8+, no dependencies):

```bash
pip install https://propertiesapi.com/sdk/propertiesapi-0.2.0-py3-none-any.whl
```

The sdist is at `https://propertiesapi.com/sdk/propertiesapi-0.2.0.tar.gz`, and the SHA-256 checksums of both are in
[`SHA256SUMS`](https://propertiesapi.com/sdk/SHA256SUMS). To pin it in `requirements.txt`, add the URL with its hash:
`propertiesapi @ https://propertiesapi.com/sdk/propertiesapi-0.2.0-py3-none-any.whl --hash=sha256:<from SHA256SUMS>`.
The package isn't on PyPI.

## Quick start

Get a key on your [dashboard](https://propertiesapi.com/dashboard) and put it in `PROPERTIESAPI_KEY`.

```python
from propertiesapi import PropertiesAPI

client = PropertiesAPI()  # reads PROPERTIESAPI_KEY; or PropertiesAPI("your-key")

home = client.get_property(url="https://www.zillow.com/homedetails/2640-Steiner-St-San-Francisco-CA-94115/15073305_zpid/")
print(home.data["price"], home.data["zestimate"], home.data["rentZestimate"])
print("from cache" if home.cached else "live lookup", "| credits left:", client.credits_remaining)

# by zpid or by address (include the unit for condos and apartments)
client.get_property(zpid="15073305")
client.get_property(address="2640 Steiner St, San Francisco, CA 94115", status="for_sale")

# figures PropertiesAPI calculates from the returned fields (not Zillow estimates); None when an input is missing
print(home.data["calculated"]["price_per_sqft"], home.data["calculated"]["gross_yield_pct"])

# an apartment building has no zpid: home.id is the zpid, or the building's detailUrl
building = client.get_property(url="https://www.zillow.com/apartments/chicago-il/sage-west-loop/CkBH79/")
print(building.data["zpid"], building.id)   # None https://www.zillow.com/apartments/...

# raw=True adds data["raw"], the unmapped source record. UNSTABLE: its keys depend on which source served the
# lookup and change without notice, so don't parse it.
client.get_property(zpid="15073305", raw=True)
```

`home.data` is a dict with camelCase fields (typed as `propertiesapi.Property`). Fields Zillow doesn't show for a
property are `None`; don't treat `None` as zero. `zpid` is `None` for an apartment building (use `home.id`).
`daysOnZillow` is Zillow's own count and is usually set for sold and off-market homes too; for days on market use
`calculated["days_on_market"]`. `home.meta` has `credits`, `cached`, `cache_key`, `fetched_at`, `source`, `release`,
`took_ms` and `completeness` (which of the commonly missing fields this response supplied: they can depend on how the
home was first looked up), plus `warning` when something needs checking. See every field on the
[property endpoint](https://propertiesapi.com/docs/endpoints/property) page.

## Batch

```python
result = client.get_properties([
    "https://www.zillow.com/homedetails/2640-Steiner-St-San-Francisco-CA-94115/15073305_zpid/",
    "2640 Steiner St, San Francisco, CA 94115",
    {"zpid": "15073305"},
])
for item in result.results:          # one per input, in your order
    if item.ok:
        print(item.index, item.id, item.data["price"])     # id: zpid, or detailUrl for a building
    else:
        print(item.index, "failed:", type(item.error).__name__, item.error.code, item.error.status)
print(result.meta)   # count, succeeded, failed, cache_hits, credits, release, took_ms (+ credits_remaining,
                     # credits_exhausted / rate_limited: {"items": [...]} when those stopped some items)
```

The API takes up to 50 items per request; `get_properties` splits longer lists into several requests and merges the
results (`meta["credits_exhausted"]["items"]` and `meta["rate_limited"]["items"]` are indexes in your list). A failed
item never raises: check `item.ok`. Its `error` is the same class, with the same `status`, as the same error on a
single lookup (a `zpid_mismatch` item is a `ZpidMismatchError`). Items with error code `batch_deadline` didn't start
in time; retry those.

If the first request fails, its error is raised. If a later request fails (a 504, a timeout, or a 429/503 still
failing after retries), you still get the results already received, which were billed: every item from the failed
request on has `ok=False` with that request's error, `result.error` is set and `result.complete` is `False`.

```python
if not result.complete:
    retry = [items[x.index] for x in result.failed if x.error is result.error]
```

## Errors

Every API error raises a subclass of `PropertiesAPIError` with `status`, `code`, `message`, `details`, `headers`,
`retry_after`, `retryable` and `credits` (the error response's credit headers):

| Exception | When |
|---|---|
| `InvalidRequestError` | 400 `missing_param`, `bad_param`, `bad_body`, `too_many_items`; 413 response too large |
| `AuthenticationError` | 401 `missing_api_key`, `invalid_api_key`, `revoked_api_key` |
| `CreditsExhaustedError` | 402 `credits_exhausted` (`.upgrade_url`) |
| `SpendLimitReachedError` | 402 `spend_limit_reached` (`.dashboard_url`) |
| `PaymentDisputedError` | 402 `payment_disputed`: a payment for the plan was disputed or refunded; plan lookups paused, prepaid credits still usable (`.dashboard_url`). Not retried |
| `NotFoundError` | 404 `not_found` |
| `ConflictError` | 409: the base class of the two below (`.candidate`) |
| `AddressAmbiguousError` | 409 `address_ambiguous` (`.candidate`, `.mismatch`) |
| `ZpidMismatchError` | 409 `zpid_mismatch`: Zillow answered the link or zpid with another home (`.candidate` has its zpid and detailUrl). Never an `AddressAmbiguousError` |
| `UnprocessableInputError` | 422 `unprocessable_input`, `upstream_unsupported_page` |
| `RateLimitedError` | 429 `rate_limited` (`.retry_after`) |
| `DailyShareExhaustedError` | 429 `daily_share_exhausted`, `env_key_daily_cap` (a subclass of `RateLimitedError`) |
| `TooManyFailedLookupsError` | 429 `too_many_failed_lookups`: too many of today's live lookups found nothing or failed; uncached lookups pause until midnight UTC (a subclass of `RateLimitedError`). Retried only when `.retry_after` is under an hour |
| `ServerError` | 500 `internal` |
| `UpstreamError` | 502 `upstream_*` |
| `ServiceUnavailableError` | 503 (`.retry_after` on `upstream_busy`, `store_unavailable`, `upstream_unavailable`, `live_lookups_capped`) |
| `GatewayTimeoutError` | 504 `upstream_timeout`, batch items with `batch_deadline`; 408 `request_timeout` |
| `APITimeoutError` | the client's own timeout fired |
| `APIConnectionError` | no HTTP response (DNS, connection refused, TLS) |

An address whose closest Zillow match is a different home raises `AddressAmbiguousError`. It isn't billed:

```python
from propertiesapi import AddressAmbiguousError, NotFoundError

try:
    home = client.get_property(address="2640 Steiner St, San Francisco, CA 94115")
except AddressAmbiguousError as e:
    print("Zillow's closest match is", e.candidate["detailUrl"], "| differs in", e.mismatch)
    # if that's the home you meant, look it up by its link (a normal, billed lookup)
    home = client.get_property(url=e.candidate["detailUrl"])
except NotFoundError:
    home = None
```

Errors aren't billed. The full list of codes is in the [docs](https://propertiesapi.com/docs#errors).

## Retries and timeouts

```python
client = PropertiesAPI(
    timeout=120,         # seconds, single lookups (default 120)
    batch_timeout=320,   # seconds, batch requests (default 320)
    max_retries=2,       # retries of retryable errors (default 2)
    max_retry_wait=60,   # don't retry if the server asks to wait longer than this (default 60)
)
```

A live lookup typically takes 8 seconds, up to about 20; an occasional one takes longer, so keep the timeout at 120
seconds or more. Only retryable errors (`err.retryable`, the codes in `propertiesapi.RETRYABLE_CODES`: `rate_limited`,
`daily_share_exhausted`, `env_key_daily_cap`, `upstream_busy`, `store_unavailable`, `upstream_unavailable`, `live_lookups_capped`, and an
unknown code on a 429 or 503) are retried, after `retry-after` seconds (or 1 s, then 2 s, without it). The daily caps
answer with a `retry-after` of hours; those are raised straight away instead of sleeping. 4xx, 500, 502, 504 and the
503 codes that mean live lookups are off (`no_api_keys`, `live_lookups_disabled`, `upstream_auth`, `upstream_quota`,
`upstream_misconfigured`) are never retried.

## Credits

```python
client.credits_remaining   # included credits left, from the last response (None before the first call)
client.credits             # Credits(remaining, limit, used, reset, overage_remaining, spend_limit_usd,
                           #         rate_limit, rate_limit_remaining, prepaid_remaining)
err.credits                # the same, from an error response (None for batch items and auth errors)
```

1 credit per successful live lookup. Repeat lookups of the same home within 24 hours come from cache and are free,
and errors are free. `meta["credits"]` says what each call cost. `prepaid_remaining` is the prepaid
(pay-as-you-go) credits left, used after the included ones. The `X-Credits-*` headers come with account (dashboard)
keys; every authenticated request gets the `X-RateLimit-*` ones.

## Other endpoints

`client.request(method, path, query=..., json_body=...)` calls any endpoint with the same auth, retries, errors and
credit tracking, and returns an `APIResponse(status, body, headers, credits)`.

## Development

```bash
cd sdks/python
python3 -m unittest discover -s tests -v   # runs against a local stub server; no key, no network
```

`tests/test_contract.py` runs the client against real API responses: the captured bodies and headers in
`sdks/contract/fixtures/live/`, and every documented error code as the server builds it
(`sdks/contract/fixtures/server.json`, regenerated by `web/scripts/build-sdk-contract.mjs`). See
`sdks/contract/README.md`.

## Support

support@propertiesapi.com · [propertiesapi.com/docs](https://propertiesapi.com/docs) · MIT license.

The data is provided as-is, as shown on the public Zillow page at the time of the lookup. It isn't official and
isn't guaranteed to be accurate or complete.
