Metadata-Version: 2.4
Name: hostily-captcha
Version: 0.1.0
Summary: Server-side client for hostily Captcha (captcha.hostily.de): verify the token of a submitted form.
Author: connect - Media & Teledienst
License-Expression: MIT
Project-URL: Homepage, https://captcha.hostily.de
Project-URL: Source, https://github.com/connect-gruppe/hostily-captcha-python
Project-URL: Issues, https://github.com/connect-gruppe/hostily-captcha-python/issues
Keywords: captcha,spam,recaptcha-alternative,proof-of-work
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: httpx
Requires-Dist: httpx>=0.24; extra == "httpx"
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Requires-Dist: pytest-asyncio>=0.21; extra == "test"
Requires-Dist: httpx>=0.24; extra == "test"
Dynamic: license-file

# hostily-captcha (Python)

**Deutsch**

Kleiner Client für [hostily Captcha](https://captcha.hostily.de). Python ≥ 3.9, nur Standardbibliothek; `httpx` wird für die
asynchrone Variante genutzt, wenn es installiert ist (sonst läuft die Standardbibliothek in einem Thread).

```sh
pip install hostily-captcha          # optional: pip install "hostily-captcha[httpx]"
```

```python
from hostily_captcha import Client, UnavailableError

captcha = Client(os.environ["HOSTILY_CAPTCHA_SECRET"])           # Secret nur auf dem Server!
try:
    result = captcha.verify_form(request.form, remoteip=request.remote_addr)
except UnavailableError:
    ...  # Dienst nicht erreichbar: blockieren oder durchlassen - Ihre Entscheidung
if result.success:
    ...
else:
    print(result.gruende, result.punkte)
```

Async: `from hostily_captcha import AsyncClient` und `await AsyncClient(secret).verify_form(form, remoteip)`.

* `verify(token, remoteip=None, formular=None)`; `verify_form(form, remoteip=None, send_content=True)` holt Token
  (`hostily-captcha-token`, sonst `g-recaptcha-response`) und Inhalt aus einem Mapping (dict, Django-`QueryDict`, Flask-`form`, …).
* `token_from(form)` und `content(form)`: Letzteres entfernt Passwortfelder, Secrets, CSRF-Token, Kartencodes und die Token-Felder.
  Passwörter werden nie mitgeschickt.
* `Result`: `success` (auch als `bool(result)`), `gruende`, `punkte`, `error_codes`, `hostname`, `is_configuration_error`.
* `timeout=` in Sekunden (Vorgabe 5), `endpoint=` für eine andere Adresse. Ohne Token wird der Dienst nicht gefragt (`token_fehlt`).
  Ein Token gilt nur für eine Prüfung.
* Tests: `pip install -e ".[test]" && pytest`.

**English**

Small client for [hostily Captcha](https://captcha.hostily.de). Python ≥ 3.9, standard library only; `httpx` is used by the
async client when installed (otherwise the standard library runs in a worker thread). Install and use as above.

* `verify(token, remoteip=None, formular=None)`; `verify_form(form, remoteip=None, send_content=True)` takes the token
  (`hostily-captcha-token`, else `g-recaptcha-response`) and the content from any mapping (dict, Django `QueryDict`, Flask `form`, …).
* `token_from(form)` and `content(form)`; the latter drops password fields, secrets, CSRF tokens, card codes and the token fields.
  Passwords are never sent.
* `Result`: `success` (also `bool(result)`), `gruende`, `punkte`, `error_codes`, `hostname`, `is_configuration_error`.
* `UnavailableError`: service unreachable / timeout / HTTP 5xx or 429 / unreadable answer - you decide to block or let pass.
* `timeout=` in seconds (default 5), `endpoint=` for another address. Without a token the service is not called (`token_fehlt`).
  A token is valid for one check only. Tests: `pip install -e ".[test]" && pytest`.

MIT License.
