Metadata-Version: 2.5
Name: isopleth
Version: 0.1.0
Summary: Official Python client for the Isopleth API: public location data and parcel records for any US point, plus ready-made tools for OpenAI, Anthropic and LangChain agents.
Project-URL: Homepage, https://isopleth.dev
Project-URL: Documentation, https://isopleth.dev/docs/agents
Project-URL: Source, https://github.com/slurpsyrup/isopleth-api/tree/main/clients/python
Project-URL: MCP server, https://isopleth.dev/docs/mcp
Author-email: Luxitag LLC <support@isopleth.dev>
License-Expression: MIT
License-File: LICENSE
Keywords: ai agents,flood,geospatial,gis,isopleth,llm tools,location data,mcp,parcels
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 :: Scientific/Engineering :: GIS
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.23
Requires-Dist: typing-extensions>=4.5; python_version < '3.11'
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
Description-Content-Type: text/markdown

# isopleth

Official Python client for the [Isopleth API](https://isopleth.dev): public-dataset facts for any US
location (terrain, flood, wildfire, weather, power grid, utilities, environment, demographics and more) and
public parcel records (owner, values, last sale, boundary) in the states that publish them. Every value
comes with its source, license and confidence.

## Quickstart

```bash
pip install isopleth
export ISOPLETH_API_KEY=isp_live_...   # free key: https://isopleth.dev/signup/ (ISOPLETH_KEY also works)
```

```python
from isopleth import Isopleth
iso = Isopleth()
print(iso.fetch("1600 Pennsylvania Ave NW, Washington, DC", preset="flood_risk")["fields"])
```

## What's in it

| Method | API | Cost |
| --- | --- | --- |
| `fetch(location, fields=..., preset=...)` | `POST /v1/fetch` | 1 lookup per started block of 50 fields |
| `fetch_batch(locations, ...)` | `POST /v1/fetch/batch` | 1 lookup per location per 50 fields |
| `geocode(address)` | `POST /v1/geocode` | 1 lookup when it resolves |
| `enrich(location \| parcel_id= \| apn=, state=)` | `POST /v1/enrich` | 5 lookups per matched parcel |
| `search(state=..., acres_min=..., ...)` | `POST /v1/search` | 25 lookups per page with results |
| `search_iter(...)` / `search_pages(...)` | paginates `next_cursor` | per page, as above |
| `coverage(state=None)` | `GET /v1/parcels/coverage` | free |
| `fields()`, `search_fields(q)`, `presets()` | `GET /v1/meta/fields` | free |
| `usage()` | `GET /v1/usage` | free |

A location is a `(lat, lng)` tuple, a `{"lat": .., "lng": ..}` dict or a US street address string.

```python
iso.fetch((30.2672, -97.7431), fields=["elevation", "nfhl_flood_zone"])
iso.fetch_batch([(35.1, -101.9), "500 E 11th St, Austin, TX"], preset="terrain")
parcel = iso.enrich((32.7767, -96.7970))
for p in iso.search_iter(state="TX", county="Travis", acres_min=40, limit=100, max_pages=3):
    print(p["parcel_id"], p["fields"].get("owner"), p["fields"].get("gisacre"))
```

### Async

```python
from isopleth import AsyncIsopleth
async with AsyncIsopleth() as iso:
    r = await iso.fetch((40.7128, -74.0060), preset="natural_hazard")
    async for p in iso.search_iter(state="NC", owner="smith", max_pages=1):
        ...
```

### Errors, retries and idempotency

Errors are typed and carry the API's `code`, `message`, `status` and `request_id`:

```python
from isopleth import QuotaExhaustedError, RateLimitError, IsoplethError
try:
    iso.search(state="TX", acres_min=1000)
except QuotaExhaustedError as e:      # 402: e.needed, e.remaining
    ...
except RateLimitError as e:           # 429: e.retry_after (seconds); DailyCapReachedError is a subclass
    ...
```

Rate limits (429), busy/upstream errors (502/503/504) and connection failures are retried up to
`max_retries=2` times, honouring `Retry-After` (up to `max_retry_delay=60` s) with jittered exponential
backoff otherwise. `fetch_batch`, `enrich` and `search` send an `Idempotency-Key` on every call (pass your own
with `idempotency_key=`), so a retry is never charged twice.

Options: `Isopleth(api_key=..., base_url=..., timeout=120, max_retries=2, http_client=httpx.Client(...))`.
The key is read from `ISOPLETH_API_KEY` when not passed and is never included in `repr()` or errors.

## Agents

The same tools as the hosted [MCP server](https://isopleth.dev/docs/mcp) (`https://api.isopleth.dev/mcp`),
as tool schemas plus a dispatcher:

```python
import anthropic
from isopleth import Isopleth
from isopleth.tools import INSTRUCTIONS, anthropic_tools, call_tool

iso, claude = Isopleth(), anthropic.Anthropic()
msgs = [{"role": "user", "content": "Is 2100 E Cesar Chavez St, Austin TX in a flood zone?"}]
while True:
    r = claude.messages.create(model="claude-sonnet-4-5", max_tokens=1024, system=INSTRUCTIONS,
                               tools=anthropic_tools(), messages=msgs)
    msgs.append({"role": "assistant", "content": r.content})
    calls = [b for b in r.content if b.type == "tool_use"]
    if not calls:
        break
    msgs.append({"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": b.id, "content": str(call_tool(iso, b.name, b.input))}
        for b in calls]})
```

- `openai_tools()` (Chat Completions), `openai_responses_tools()` (Responses API), `anthropic_tools()`,
  `tool_specs()` (neutral JSON Schema); pass `names=[...]` for a subset.
- `call_tool(client, name, arguments)` accepts a dict or the model's JSON string and returns a dict; API errors
  come back as `{"error": {...}}` so the model can react. `acall_tool` is the async version.
- LangChain: `pip install "isopleth[langchain]"`, then
  `from isopleth.langchain import IsoplethToolkit; tools = IsoplethToolkit().get_tools()`.

## License

MIT © Luxitag LLC. Data returned by the API carries its own per-source license and attribution.
