Metadata-Version: 2.4
Name: genderapi
Version: 2.0.0
Summary: Official GenderAPI.io V2 client for Python
Author-email: Onur Ozturk <support@genderapi.io>
License: MIT
Project-URL: Homepage, https://www.genderapi.io/api-documentation
Project-URL: Documentation, https://www.genderapi.io/docs/v2/responses
Project-URL: Repository, https://github.com/GenderAPI/genderapi-python
Project-URL: Bug Tracker, https://github.com/GenderAPI/genderapi-python/issues
Keywords: gender,api,genderapi,gender inference,name,email,username
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# genderapi-python

Official GenderAPI.io V2 client for Python.

It sends names, email addresses and usernames to the [GenderAPI.io V2 API](https://www.genderapi.io/api-documentation) and returns the complete V2 response with typed accessors. Results are inferences and can be `unknown`. They do not verify a person's identity.

- Python 3.9 or newer, no runtime dependencies (standard library `urllib`), type hints included (`py.typed`).
- **Server-side only.** Never put an API key in browser, mobile or other client-side code.
- Version 2.x targets the V2 API. 1.x (V1 API) stays available and installable indefinitely; no deprecation or shutdown is planned. To keep using it, pin 1.x: `pip install "genderapi<2"`. The source stays on the [`v1` branch](https://github.com/GenderAPI/genderapi-python/tree/v1). See [Migrating from 1.x](#migrating-from-1x).

## Install

```bash
pip install "genderapi>=2,<3"
```

## Quick start

```python
from genderapi import GenderAPI

client = GenderAPI()  # reads GENDERAPI_API_KEY from the environment

result = client.name("Alice", country="US")
print(result.data.gender)            # "male", "female" or None
print(result.data.result_status)     # "identified" or "unknown"
print(result.data.confidence, result.data.confidence_kind)
print(result.meta.usage.billing_status, result.meta.usage.charged_credits)
```

Email and username:

```python
client.email("alice.smith@example.com")
client.username("prenses", country="TR", force_to_genderize=True)
client.gender("name", "Alice", ai_mode="off")   # generic form
```

### Batch

```python
from genderapi import make_item

batch = client.gender_batch([
    make_item("name", "Alice", country="US", id="row-1"),
    make_item("email", "alice.smith@example.com", id="row-2"),
    {"type": "username", "value": "prenses", "id": "row-3", "options": {"ai_mode": "fallback"}},
])

for item in batch.data:
    if item.error:
        print(item.index, item.id, "failed:", item.error.code)
    else:
        print(item.index, item.id, item.data.gender, item.data.result_status)

print(batch.summary.total, batch.summary.succeeded, batch.summary.failed)
print(batch.meta.usage.charged_credits)
```

A batch accepts 1-50 items (10 on the IP trial; the server enforces that limit). Split larger jobs yourself. Items are plain dicts with the exact V2 wire field names; `make_item()` builds and validates them.

### Usage (free)

```python
usage = client.usage()
print(usage.data.remaining_credits, usage.meta.access.mode)
```

### Other endpoints

```python
client.validate_phone("+90 555 111 22 33")   # structure check, not subscriber existence
client.capabilities()                        # GET /api/v2 (public, no key sent)
client.error_catalog()                       # GET /api/v2/errors (public, no key sent)
```

## API key and IP trial

The key comes from the constructor (`GenderAPI("your-key")`) or the `GENDERAPI_API_KEY` environment variable. It is sent only as `Authorization: Bearer <key>`, never in a URL, and never appears in `repr()` or exception messages.

Without a key the client still works: the server applies its shared IP trial (10 credits per 24 hours at the time of writing). The client has no trial logic of its own. Check `result.meta.access.mode` (`api_key`, `ip_trial` or `unauthenticated`) and `meta.access.reason` (`api_key_missing`, `api_key_invalid`, `api_key_not_found`) to see which access applied. An unrecognised key can fall back to the IP trial. When a key is configured, the client therefore raises `UnexpectedAccessModeError` if a successful response reports any `access.mode` other than `api_key` (see [Errors](#errors)); pass `require_api_key_access=False` to receive such responses normally instead.

## Options

| Constructor argument | Default | Notes |
| --- | --- | --- |
| `api_key` | `GENDERAPI_API_KEY` | Optional. Without it the IP trial applies. |
| `timeout` | `10` seconds | Applies to each blocking socket operation (connect, send, each read), not the whole request. |
| `base_url` | `https://api.genderapi.io/api/v2` | Must be HTTPS. `http://localhost`, `127.0.0.1` and `[::1]` are accepted for local test servers only. |
| `user_agent` | `None` | Appended to `genderapi-python/2.0.0`. |
| `require_api_key_access` | `True` | Only effective when a key is configured. Raises `UnexpectedAccessModeError` when a successful response reports `meta.access.mode` other than `api_key` (for example `ip_trial` because the key was not accepted). Never applies to `capabilities()` or `error_catalog()`. Set `False` to return such responses normally. |

| Prediction argument | Wire field | Values |
| --- | --- | --- |
| `type` | `type` | `name`, `email`, `username` |
| `value` | `value` | 1-254 characters, no control characters |
| `country` | `country` | Optional ISO 3166-1 alpha-2, uppercase (`"US"`). Omit when unknown. |
| `ai_mode` | `options.ai_mode` | `off`, `fallback`, `always`. Omitted: server default (`fallback` for single requests, `off` for batch items). |
| `force_to_genderize` | `forceToGenderize` | Dataset first, then nickname-aware AI. Cannot be combined with `ai_mode` `off` or `always`. |
| `id` | `id` | Optional, 1-64 characters, unique within a batch. |

Invalid input raises `genderapi.ValidationError` (also a `ValueError`) before any request is sent. The server performs the authoritative validation, for example of email syntax and country codes.

Credit costs (server rules, see [Credits & balance](https://www.genderapi.io/docs/v2/credits-and-usage)): an ordinary request costs 1 credit, including an AI fallback; `ai_mode="always"` costs 2; `forceToGenderize` costs 1 when the dataset resolves the gender and 2 when AI is used. Successful `unknown` results are billable. A positive starting balance is enough, so the balance can become negative (for example 1 - 2 = -1).

## Response fields

Every response object keeps the full JSON in `.raw` (also available as `obj["field"]`, `obj.get("field")` and `obj.to_dict()`), so fields added later are never lost. Unknown fields are ignored by the typed accessors, not rejected.

`GenderResponse.data` (`Prediction`):

| Field | Meaning |
| --- | --- |
| `gender` | `"male"`, `"female"` or `None` |
| `result_status` | `"identified"` or `"unknown"`. An unknown result is a successful, billable response, not an error. |
| `reason` | `None` when identified; `not_found`, `no_name_candidate`, `ambiguous` or `insufficient_evidence` when unknown |
| `confidence` | 0-1 value, or `None`. Returned exactly as sent; the client never converts it into a probability or percentage. |
| `confidence_kind` | `observed_frequency` (dataset share) or `model_reported` (a model score, not a calibrated probability) |
| `sample_count` | Dataset sample count, `None` for AI and unknown results |
| `source` | `dataset`, `ai` or `none` |
| `name` | Returned dataset name or extracted given name, may be `None` |
| `country`, `country_source` | Country associated with the result and where it came from (`dataset`, `ai_association` or `None`). Neither indicates nationality, residence or ethnicity. |
| `match` | `name`, `method` (`normalized`, `token`, `substring`, `model_inference`), `scope` (`country`, `global`), `country` |
| `input` | `type`, `value`, `country`, `force_to_genderize` (wire: `forceToGenderize`) |

`meta` (`Meta`): `request_id`, `duration_ms`, `access` (`mode`, `reason`), `usage` and, for batches, `summary`.

`meta.usage` (`Usage`):

| Field | Meaning |
| --- | --- |
| `billing_status` | `not_charged`, `confirmed` or `unconfirmed` |
| `charged_credits` | Net charge of this request; `None` while billing is unconfirmed |
| `remaining_credits` | Balance at completion; can be negative, `None` when unknown |
| `resets_at`, `limit`, `period_seconds` | IP trial window; `None` otherwise (not a subscription expiry) |

`BatchResponse`: `data` (list of `BatchItem`, also `.items`), `meta`, `summary` (`total`, `succeeded`, `identified`, `unknown`, `failed`), `succeeded_items`, `failed_items`, `has_failures`. Each `BatchItem` has `index`, `id`, `charged_credits` and exactly one of `data` (a `Prediction`) or `error` (a `Problem`).

See [Read a response](https://www.genderapi.io/docs/v2/responses) for the full contract.

## Errors

```python
from genderapi import (
    GenderAPIError, ValidationError, APIError, RateLimitError,
    InsufficientCreditsError, ServerError, TransportError, APITimeoutError,
    UnexpectedAccessModeError,
)

try:
    result = client.name("Alice")
except ValidationError as exc:
    ...                                  # fix the input; nothing was sent
except RateLimitError as exc:
    wait = exc.retry_after_seconds       # or exc.retry_after (raw header)
except InsufficientCreditsError as exc:
    print(exc.usage.remaining_credits, exc.usage.resets_at)
except UnexpectedAccessModeError as exc:
    print(exc.access_mode, exc.access_reason)  # key not accepted; already processed
    result = exc.result                  # the complete result, possibly billed to the IP trial
except APIError as exc:
    print(exc.status, exc.code, exc.action, exc.billing_status, exc.request_id)
    for field_error in exc.errors:       # validation pointers
        print(field_error.pointer, field_error.message)
except APITimeoutError:
    ...                                  # outcome unknown: check client.usage() before resending
except TransportError:
    ...                                  # outcome unknown: check client.usage() before resending
```

| Exception | When |
| --- | --- |
| `ValidationError` | Local input check failed. No request was sent. |
| `APIError` | HTTP status >= 400. Subclasses: `AuthenticationError` (401), `PermissionDeniedError` (403), `InsufficientCreditsError` (403 `insufficient_credits`), `UnprocessableEntityError` (422), `RateLimitError` (429), `ServerError` (5xx). |
| `RedirectError` | The server answered with a 3xx. Redirects are never followed, so the key is never forwarded. |
| `TransportError` / `APITimeoutError` | No usable response (connection error, timeout). The request may still have been completed and billed. |
| `InvalidResponseError` | The response was not the expected JSON object. |
| `UnexpectedAccessModeError` | A key is configured, but a successful response reports `meta.access.mode` other than `api_key` (usually `ip_trial` because the key was not accepted). The request has already been processed and may have consumed IP-trial credits; it is not retried. |

`APIError` exposes `status`, `code`, `title`, `detail`, `type`, `instance`, `action`, `documentation`, `errors`, `request_id` (from the body, `meta.request_id` or the `X-Request-ID` header), `retry_after` / `retry_after_seconds` (from the `Retry-After` header), `meta`, `usage`, `access`, `billing_status`, `is_billing_unconfirmed`, `body` (parsed JSON), `raw_body` (bytes) and `headers`. Match on `code` and `action`, never on `detail` text. Proxy-level failures can return non-JSON bodies; then `code` is `None` and `raw_body` holds the response. The full list of codes is in the [error catalog](https://api.genderapi.io/api/v2/errors) (`client.error_catalog()`).

`UnexpectedAccessModeError` has `code` (`"unexpected_access_mode"`), `access_mode`, `access_reason` (for example `api_key_invalid`) and `result`, the complete parsed result the method would have returned (`GenderResponse`, `BatchResponse`, `UsageResponse` or `PhoneResponse`), plus `status`, `request_id`, `headers`, `body` and `raw_body`. Disable the check with `GenderAPI(require_api_key_access=False)`.

When every executed batch item fails, the server returns an error status and the client raises `APIError`; `exc.data` still holds the item outcomes and `exc.summary` the counts. A partial batch success is **not** an exception.

## Billing and retries

The client **never retries automatically**, including on 429, 5xx and timeouts. A lost response can still mean a completed, billed request, and every new request is a new operation with normal billing.

- **429**: wait for `retry_after` before sending a new request.
- **`billing_status == "unconfirmed"`** (for example `billing_reconciliation_required`): do not retry; contact support with `request_id`.
- **Other prediction failures**: inspect `billing_status` and fix the cause before sending again.
- **Partial batch success**: retry only the failed items, and only once billing is confirmed. Resubmitting successful items charges them again.
- **Timeouts and transport errors**: call `client.usage()` to check the balance before resending.

See [Errors & retries](https://www.genderapi.io/docs/v2/errors-and-retries).

## Security

- Use the client on servers only. Keep the key in an environment variable or secret store, never in source control or client-side code.
- The client does not log requests, responses or the key. Treat response bodies as personal data where applicable and do not log them blindly.
- HTTPS is required for non-local base URLs, and redirects are rejected.
- Importing the package or constructing a client makes no network request.

## Migrating from 1.x

Version 2.0.0 is a breaking release for the V2 API. The same API key and credit balance work with both versions. 1.x (V1 API) stays available and installable indefinitely, with no deprecation or shutdown planned. To keep using it, pin 1.x:

```bash
pip install "genderapi<2"
```

The 1.x source stays on the [`v1` branch](https://github.com/GenderAPI/genderapi-python/tree/v1).

| 1.x (V1 API) | 2.x (V2 API) |
| --- | --- |
| `GenderAPI(api_key, base_url="https://api.genderapi.io")` | `GenderAPI(api_key=None, timeout=10, base_url="https://api.genderapi.io/api/v2")`; key also from `GENDERAPI_API_KEY` |
| `get_gender_by_name(name, country, askToAI, forceToGenderize)` | `name(value, country=..., ai_mode=..., force_to_genderize=...)` or `gender("name", value, ...)` |
| `get_gender_by_email(email, country, askToAI)` | `email(value, ...)` |
| `get_gender_by_username(username, country, askToAI, forceToGenderize)` | `username(value, ...)` |
| `get_gender_by_name_bulk(data)` / `..._email_bulk` / `..._username_bulk` | `gender_batch(items)`; input types can be mixed in one batch |
| V1 routes `/api`, `/api/email`, `/api/username`, `/api/.../multi/country` | `POST /api/v2/gender`, `POST /api/v2/gender/batch` |
| `/api/phone` | `validate_phone(number, country)` (`POST /api/v2/phone/validate`) |
| `askToAI=True` | `ai_mode="fallback"` (the single-request default) or `ai_mode="always"` |
| `forceToGenderize` | `force_to_genderize` (wire: `forceToGenderize`), same meaning for name, email and username |
| Flat response dict | `result.data` (prediction) and `result.meta` (request, access, billing) |
| `probability` (percentage) | `data.confidence` (0-1) plus `data.confidence_kind`; AI scores are not calibrated probabilities |
| `total_names` | `data.sample_count` (nullable) |
| `status` / `errno` / `errmsg` in the body | HTTP status plus Problem Details `code` and `action`, raised as `APIError` subclasses |
| `used_credits` | `meta.usage.charged_credits` (plus `billing_status`) |
| `remaining_credits` | `meta.usage.remaining_credits` (can be negative or `None`) |
| `requests` dependency | No runtime dependencies |
| Python >= 3.7 | Python >= 3.9 |

See [Migrate from V1](https://www.genderapi.io/docs/v2/migration) and the [V1 documentation](https://www.genderapi.io/api-documentation/v1) for the V1 contract.

## Development

```bash
python -m unittest discover -s tests -t . -v   # local stub server, no real API calls or credits
python -m build                                 # sdist and wheel in dist/
```

Test fixtures in `tests/fixtures/` are copies of the published `openapi-v2.json` and error catalog.

## Links

- API documentation: https://www.genderapi.io/api-documentation
- V2 guides: [authentication](https://www.genderapi.io/docs/v2/authentication), [request parameters](https://www.genderapi.io/docs/v2/request-parameters), [batch](https://www.genderapi.io/docs/v2/batch), [responses](https://www.genderapi.io/docs/v2/responses), [AI options](https://www.genderapi.io/docs/v2/ai-options), [credits & usage](https://www.genderapi.io/docs/v2/credits-and-usage), [errors & retries](https://www.genderapi.io/docs/v2/errors-and-retries), [phone validation](https://www.genderapi.io/docs/v2/phone-validation), [migration](https://www.genderapi.io/docs/v2/migration)
- OpenAPI: https://api.genderapi.io/api/v2/openapi.json

## License

MIT
