Metadata-Version: 2.4
Name: gtmfree
Version: 0.7.0
Summary: Python SDK for the gtm:free REST API
Project-URL: Homepage, https://gtmfree.com
Project-URL: Documentation, https://gtmfree.com/docs/api/
Project-URL: Repository, https://github.com/gtmfree/gateway
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: urllib3<3,>=2.1
Requires-Dist: python-dateutil<3,>=2.8.2
Requires-Dist: pydantic<3,>=2.11
Requires-Dist: typing-extensions<5,>=4.7.1

# gtm:free Python SDK

Version 0.7.0. Generated from the reviewed REST contract with OpenAPI Generator. Python 3.10+; synchronous urllib3 transport with Pydantic models. This package has not been published to PyPI yet.

From the monorepo checkout:

```sh
python -m pip install ./sdk/python
```

```python
import os
from gtmfree import ApiClient, Configuration
from gtmfree.api.account_api import AccountApi
from gtmfree.api.google_maps_api import GoogleMapsApi
from gtmfree.api.jobs_api import JobsApi
from gtmfree.models.google_maps_job_request import GoogleMapsJobRequest

config = Configuration(
    host="https://gtmfree.com/api/v1",  # Local: http://localhost:3000/api/v1
    access_token=os.environ["GTMFREE_API_KEY"],  # Raw key; Bearer is added for you.
)
with ApiClient(config) as client:
    me = AccountApi(client).get_current_user(_request_timeout=30.0)
    # This next call starts a new job. After a lost response, list your jobs first.
    accepted = GoogleMapsApi(client).create_google_maps_job(
        google_maps_job_request=GoogleMapsJobRequest(
            parameters={"query": "dentists", "region": "poland"},
            exports=["csv"],
        ),
        _request_timeout=30.0,
    )
    job = JobsApi(client).get_job(accepted.job_id, _request_timeout=30.0)
```

Sign up and verify your email before creating an API key. WorkOS OAuth tokens are for MCP, not this REST SDK. All interfaces share your job ownership, one active job per tool, quotas and network restrictions.

Automatic transport retries are disabled by default, including job creation. Catch `gtmfree.exceptions.ApiException`; inspect `status`, `headers` (`Retry-After`, `X-Request-Id`), `body` and the decoded `data` error model when present. An edge rejection can return non-JSON, so decoding an error body is optional. For 503 `submission_outcome_unknown`/`submission_in_progress`, retain the owned `job_id`, poll it. After a lost response without a job ID, list your jobs before submitting again. Never infer cancellation from a disconnect.

Use `JobsApi.list_jobs(limit=50, cursor=...)` and `get_job_results(job_id, limit=50, cursor=...)`, following `next_cursor` until it is `None`. Do not parse cursors or change filters while paging. Nullable wire fields remain `None`; timestamps are timezone-aware `datetime` values.

`download_job_file(job_id, format="csv")` returns bytes, never a filename or base64. For bounded memory, use `download_job_file_without_preload_content(...)` and consume the urllib3 response in chunks. Consume SSE incrementally with `watch_job_without_preload_content(...)`; the ordinary `watch_job` method buffers until the stream ends. Always check the raw response status, then close/release the connection in a `finally` block. Set a suitable read timeout and reconnect after the server's stream lifetime. Polling every two seconds is the simpler fallback; the SDK does not poll automatically.

Jobs support up to **100,000 businesses**. Result pages contain at most 100 records; follow every cursor, including pages shorter than requested. For large exports, the streaming helper writes bytes directly to your destination:

```python
from contextlib import closing
from gtmfree.streaming import iter_job_export, iter_job_results

with closing(iter_job_export(JobsApi(client), job_id, "csv")) as chunks:
    with open("results.csv", "wb") as output:
        for chunk in chunks:
            output.write(chunk)  # At most 64 KiB per chunk.

for business in iter_job_results(JobsApi(client), job_id):
    process(business)
```

Resume a file with `iter_job_export(..., offset=bytes_already_saved)` and append the bytes; the server returns 206. The generated download operation also accepts the `Range` header. Helpers make no automatic retries. Honor 429 `Retry-After`; keep page cursors when managing pagination manually. All reads share 120 requests/minute/user, and transfers share 4 GiB/user/UTC hour, two concurrent downloads/user and a 15-minute transfer deadline. Repeated or interrupted transfers still consume their admitted bytes. A job reaching the entity cap is explicitly marked partial; no output silently claims complete coverage.

Regenerate through `scripts/generate_sdks.py`; do not edit this directory directly. Change generator settings or templates in `sdk/` instead. Package names and release publishing remain to be configured before a public release.

## Read your allowances

Every REST response includes `RateLimit-Policy`, `RateLimit`, and `GTMFree-Limits`.
Use the generated response wrapper to inspect successful responses:

```python
import json
reply = AccountApi(client).get_current_user_with_http_info()
allowances = json.loads(next(value for name, value in reply.headers.items()
                             if name.lower() == "gtmfree-limits"))
# Each allowance has limit, remaining, reset_at (Unix UTC seconds), reset_after,
# window_seconds, scope, unit, and cost. Concurrency resets are None.
```

The same headers are available on API errors and raw streaming responses. Honor
`Retry-After` on errors and nonterminal job polling. A quota snapshot is not a
reservation. `GTMFree-Limits-Status: unavailable` means user counters could not be
read; never infer unlimited capacity from an omitted counter. Edge rejections can
omit these application headers. See https://gtmfree.com/docs/limits.md.

## Countries and subdivisions

Use the generated Regions API's discoverRegions/discover_regions operation to list countries, inspect supported levels and search an exact area. Send country, level and load=true to index a supported layer; a cold lookup can return HTTP 503 with code region_loading and Retry-After: 5 while work continues. Retry the same lookup after the delay. A global search covers only already-loaded subdivisions; preserve scope and complete.

Submit region as a country string, {country: "DEU", level: 1, name: "Bavaria"}, or {id: "<exact ID from discovery>"}. Structured selection cannot also supply geodata. Never guess opaque IDs or automatically choose an ambiguous match. REST result pages include the exact selected region and original source/license notices; binary downloads link to that metadata.

Administrative boundaries: [geoBoundaries gbOpen](https://www.geoboundaries.org/) ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)). Preserve attribution and original layer notices.

Search cities worldwide with `kind="city"` and `query="Munich"` (or `"Lyon"`). Use the returned stable `geonames:…` ID with `load=true, preview=true` to resolve one municipal boundary, then submit `region={"id": city_id}`. Inspect `city.boundary_status` and preserve attribution. A preview is display-only; the worker uses its full polygon for scraping. City `level` is null. `complete=true` means this gazetteer snapshot was searched, not universal boundary coverage.
