Metadata-Version: 2.4
Name: geog-ai
Version: 0.1.0
Summary: Official Python stub client for the geog.ai Spatial Intelligence API.
Author-email: "geog.ai" <hello@geog.ai>
License: Proprietary
Project-URL: Homepage, https://geog.ai/docs/
Project-URL: Documentation, https://geog.ai/docs/
Project-URL: Source, https://geog.ai/docs/sdks/python
Keywords: geog,geog.ai,spatial,geospatial,plume,rf,jurisdiction,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
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: Topic :: Scientific/Engineering :: GIS
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28
Requires-Dist: typing_extensions>=4.5
Provides-Extra: codegen
Requires-Dist: datamodel-code-generator>=0.25; extra == "codegen"

# geog-ai (Python)

Stub client for the [geog.ai Spatial Intelligence API](https://geog.ai/docs/).
Generated from `openapi.yaml` (v1). All 28 documented endpoints are exposed as
methods on a single `GeogClient`.

> **Stub status.** Method signatures cover every documented path and method.
> Request and response payloads are plain dicts — refine to typed models with
> `pydantic` or regenerate from the OpenAPI spec when stricter types are needed.

## Install

```bash
pip install geog-ai
```

Requires Python ≥ 3.9 and `requests`.

## Usage

```python
import os
from geog_ai import GeogClient

geog = GeogClient(api_key=os.environ["GEOG_API_KEY"])

# 1. Resolve spatial context for a registered device
ctx = geog.context(device_id="sensor_h2s_023")

# 2. Kick off an async plume simulation
job = geog.simulate_plume({
    "source": {
        "lat": 31.9642, "lon": -99.9035, "alt_m": 545,
        "emission_rate_gs": 2.4, "stack_height_m": 12,
    },
    "duration_min": 60,
    "tier": 1,
    "species": "H2S",
})

# 3. Wait for completion (polls /jobs/{id})
result = geog.wait_for_job(job["job"]["id"])
print(result["result"])
```

## Async-job polling (typed result)

`/simulate/*`, `/rf/mesh/viability`, `/rf/optimize/placement` and
`/optimize/*` return the same `AsyncJobAccepted` envelope (HTTP 202) — just
an acknowledgement with a `job.id`. The actual payload lands at
`GET /jobs/{job_id}`, typed as `JobResponse[TResult]`. `wait_for_job`
returns `JobResponse[Any]`; **cast to your generic** so type-checkers
(mypy/pyright) treat `done["result"]` as your concrete shape:

```python
import os
from typing import List, TypedDict, cast
from geog_ai import GeogClient
from geog_ai.types import JobResponse


class PlumeContour(TypedDict):
    ppb: float
    geometry: dict


class PlumeResult(TypedDict):
    contours: List[PlumeContour]
    peak_ppb: float
    impacted_receptor_ids: List[str]


geog = GeogClient(api_key=os.environ["GEOG_API_KEY"])

# 1. Submit — 202 AsyncJobAccepted, no "result" yet
accepted = geog.simulate_plume({...})

# 2. Poll until terminal state and cast to your typed envelope
done = cast(
    JobResponse[PlumeResult],
    geog.wait_for_job(accepted["job"]["id"], interval=2.0, timeout=300),
)

# 3. Narrow on status before reading "result"
if done["job"]["status"] == "complete" and done.get("result"):
    result: PlumeResult = done["result"]
    print(result["peak_ppb"], result["impacted_receptor_ids"])
else:
    raise RuntimeError(f"Job {accepted['job']['id']} failed")

# One-shot snapshot (no polling loop):
snapshot = cast(JobResponse[PlumeResult], geog.job(accepted["job"]["id"]))
```

Swap `PlumeResult` for `FloodResult`, `RFCoverageResult`,
`MeshViabilityResult`, `NodePlacementResult`, etc. — the SDK shape is the
same; only your generic changes per endpoint.

> **Two envelopes, not one.** `AsyncJobAccepted` (returned immediately by
> the submit call) only carries `{"ok", "job": {"id", "status", ...}}` — no
> `"result"`. The eventual `JobResponse[TResult]` from `GET /jobs/{id}`
> only populates `"result"` when `job.status == "complete"`; on `"failed"`
> inspect `job["error"]` / `meta`.

## Errors

Non-2xx responses raise `geog_ai.GeogApiError` with `status`, `code`, `message`,
optional `details`, and `request_id` attributes.

## Regenerating types from the spec

The primitive request/response shapes (`Location`, `WindVector`, `SpatialState`,
`AsyncJobResponse`, etc.) live in `geog_ai/_openapi_gen.py`, which is
auto-generated from `QHPA/marketing/geog/docs/openapi.yaml` by
[`datamodel-code-generator`](https://pypi.org/project/datamodel-code-generator/).
The hand-written `geog_ai/types.py` re-exports these shapes under public
aliases and adds curated request envelopes for endpoints whose query/body
shapes are not modelled as named schemas in the spec.

After editing the spec, regenerate with **either**:

```bash
# install the codegen extra once
pip install -e ".[codegen]"

datamodel-codegen \
  --input ../../openapi.yaml --input-file-type openapi \
  --output geog_ai/_openapi_gen.py \
  --output-model-type typing.TypedDict \
  --target-python-version 3.10 \
  --use-schema-description --use-field-description --use-double-quotes

# or from the repo root, regenerates both SDKs in one shot
bash QHPA/marketing/geog/docs/sdks/scripts/generate-types.sh
```

`geog_ai/_openapi_gen.py` carries a `generated by datamodel-codegen` header.
Treat it as build output: never hand-edit it; change the spec and rerun the
script.

## License

Proprietary — © geog.ai. Contact `hello@geog.ai`.
