Metadata-Version: 2.5
Name: sitesonar-client
Version: 0.1.0
Summary: Typed client for the SiteSonar v2 API, generated from its OpenAPI schema
Project-URL: Repository, https://github.com/isaacgounton/sitesonar
License-Expression: MIT
Requires-Python: >=3.10
Requires-Dist: attrs>=22.2
Requires-Dist: httpx<1,>=0.23
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio<2,>=1.1; extra == 'dev'
Requires-Dist: pytest<10,>=8.4; extra == 'dev'
Description-Content-Type: text/markdown

# sitesonar-client

Typed Python client for the SiteSonar v2 API, sync and async, generated from the API's
OpenAPI schema (`../openapi.json`) by [openapi-python-client](https://github.com/openapi-generators/openapi-python-client).
Every route is a module with `sync`, `sync_detailed`, `asyncio` and `asyncio_detailed`;
bodies and responses are attrs classes, so mypy or pyright catches a misspelled field, a
wrong enum value or an attribute the response doesn't have. Runtime: `httpx` and `attrs`.

## Install

```bash
uv add sitesonar-client   # or: pip install sitesonar-client
```

## Use

```python
from sitesonar_client import connect, unwrap, wait_for_job
from sitesonar_client.api.leads import companies_search, leads_scrape
from sitesonar_client.models import CompanySearchRequest, LeadScrapeRequest, LeadScrapeResult

with connect("https://sonar.example.com", api_key) as sonar:  # no /v2
    page = unwrap(companies_search.sync_detailed(
        client=sonar,
        body=CompanySearchRequest(industry=["dentist"], city="Montreal", limit=25),
    ))
    for company in page.companies:
        print(company.name, company.email and company.email.value)

    job = unwrap(leads_scrape.sync_detailed(
        client=sonar, body=LeadScrapeRequest(industry="dentist", location="Laval")))
    done = wait_for_job(sonar, job.job_id, interval=3)
    if isinstance(done.result, LeadScrapeResult):
        print(len(done.result.leads))
```

Async is the same with `asyncio_detailed`, `async with connect(...)` and
`await wait_for_job_async(...)`.

- `unwrap(response)` returns the parsed body, or raises `SiteSonarError` with `status`,
  `code` (the API's `detail.error`: `rate_limited`, `fetch_failed`, `blocked_destination`…;
  `validation_failed` for an invalid body) and `body`.
- `wait_for_job` polls `/v2/jobs/{id}` and raises `SiteSonarError` (`job_failed`,
  `job_timeout`); `job.result` is a `CrawlResult` or a `LeadScrapeResult`.
- Fields are snake_case in Python (`review_count`) and camelCase on the wire
  (`reviewCount`); unset optional fields stay out of the request body.

Responses are typed for scrape, crawl, jobs, the leads routes, company and people search,
lookup, verdicts and usage. Other routes parse into a model whose fields live in
`.additional_properties` until their response model is added to `sitesonar/responses.py`.

## Develop

```bash
uv run --extra dev pytest        # helpers, against a mocked transport
uv run --extra dev mypy          # typing, including mistakes that must stay errors
uv run python generate.py        # regenerate after the schema changes
uv run python generate.py --check
```

`generate.py` replaces only the generated parts (`api/`, `models/`, `client.py`,
`errors.py`, `types.py`); `__init__.py` and `helpers.py` are hand-written. The server's
`tests/test_python_client.py` runs this client against the real app.
