Metadata-Version: 2.4
Name: ogu-api
Version: 0.4.0
Summary: Async Python wrapper for the OGUsers (oguser.com) forum.
Project-URL: Homepage, https://github.com/forgivenforget/ogu-api
Project-URL: Repository, https://github.com/forgivenforget/ogu-api
Project-URL: Issues, https://github.com/forgivenforget/ogu-api/issues
Author: forgivenforget
Maintainer: forgivenforget
License: MIT License
        
        Copyright (c) 2026 forgivenforget
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: forum,ogu,oguser,ogusers,sdk,wrapper
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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.11
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: tls-client>=1.0
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

# ogu-api

An async Python wrapper for the [OGUsers](https://oguser.com) forum.

Wraps the public-facing pages and forms; login, profile lookup, reputation, credits, private messages, feed, threads, notifications, member search, account control panel, with a typed, async-first client.

## Install

```bash
pip install ogu-api
```

Requires Python 3.11+.

## Quick start

```python
import asyncio

from ogu_api import OGUClient


async def main() -> None:
    async with OGUClient() as client:
        await login(client, 'username', 'password')

        user = await client.users.get_by_username('forgivenforget')
        print(user.username, user.credits, user.reputation, user.vouches)


async def login(client: OGUClient, username: str, password: str, two_factor: str = '') -> None:
    page = await client.session.get_login_page()
    hidden = client.session.extract_login_hidden(page.text)

    await client.session.login(username, password, two_factor, **hidden)


asyncio.run(main())
```

## Resources


| Resource               | What it covers                                                                        |
| ---------------------- | ------------------------------------------------------------------------------------- |
| `client.session`       | login page, login, logout                                                             |
| `client.users`         | profile lookup by username or uid; returns `UserProfile` (id, username, reputation, vouches, credits) |
| `client.usercp`        | notepad, signature, options, profile, change username/password/email                  |
| `client.reputation`    | reputation page, send reputation                                                      |
| `client.credits`       | donate page, stats page, send credits, parse recently-sent transactions               |
| `client.messages`      | PM inbox, conversation, compose, send, tracking, delete                               |
| `client.notifications` | notifications page, alerts, mark alerts read                                          |
| `client.feed`          | explore feed, home feed, thread-id extraction                                         |
| `client.threads`       | view thread, view forum, reply, create thread                                         |
| `client.search`        | full-text search form + post                                                          |
| `client.members`       | member list, top, statistics, team, groups, vouches, awards, reputation history       |


## Inbox & PMs

```python
inbox = await client.messages.inbox()

for m in inbox.messages:
    print(m.username, m.message)

await client.messages.send(
    to = 'recipient',
    message = 'hello',
    my_post_key = inbox.my_post_key,
)
```

`Inbox` is a dataclass with `messages: tuple[Message, ...]`, `conversation_ids: tuple[str, ...]`, and `my_post_key`. Most authenticated POSTs across the SDK need a `my_post_key` — `inbox.my_post_key` (or `extract_my_post_key()` from any logged-in page) gives you one.

## Read the feed

```python
for thread in await client.feed.explore():
    print(thread.title, thread.link, thread.tid)

for thread in await client.feed.home():
    print(thread.title, thread.tid)
```

`feed.explore()` and `feed.home()` return `list[ThreadSummary]` with `title`, `link`, and `tid` (numeric, or `None` for slug-rewritten URLs that don't carry one). For raw HTML access, use `get_explore()` / `get_home()`.

## Reply to a thread

```python
reply_page = await client.threads.get_reply_page(tid)
hidden = client.threads.extract_reply_hidden(reply_page.text)

await client.threads.reply(
    tid,
    message = 'great post',
    my_post_key = hidden['my_post_key'],
    post_hash = hidden.get('posthash', ''),
)
```

## Persist a session

`ogu-api` doesn't store credentials. After login, the session lives on the client's cookie jar — serialize it however you like:

```python
cookies = {cookie.name: cookie.value for cookie in client.cookies}
```

To resume a session, pre-seed the cookies on a fresh client:

```python
client = OGUClient()

for name, value in cookies.items():
    client.cookies.set(name, value)
```

## Configuration

```python
client = OGUClient(
    proxy = 'user:pass@host:8080',
    timeout_seconds = 30.0,
    max_retries = 3,
    retry_backoff_seconds = 0.5,
    client_identifier = 'chrome131',
)
```

Proxy strings accept `user:pass@host:port`, `host:port:user:pass`, or plain `host:port` and are normalized to `http://...` for the underlying `tls_client` session.

## Errors

```python
from ogu_api import OGUNotFoundError, OGURateLimitError

try:
    await client.users.get_by_username('does-not-exist')

except OGUNotFoundError as E:
    print('not found:', E.url)

except OGURateLimitError as E:
    print('rate limited, retry after', E.retry_after_seconds)
```

Full hierarchy:

- `OGUError`
  - `OGUAPIError` — `OGUAuthenticationError`, `OGUAuthorizationError`, `OGUNotFoundError`, `OGUValidationError`, `OGURateLimitError`, `OGUServerError`
  - `OGUNetworkError` — `OGUTimeoutError`
  - `OGUParseError`, `OGUSessionError`, `OGULoginError`, `OGUReputationError`, `OGUCreditsError`

## Captcha

Sending credits requires an hCaptcha token. `ogu-api` doesn't ship a solver — pass a pre-solved token in:

```python
page = await client.credits.get_donate_page()

await client.credits.send(
    username = 'recipient',
    amount = 100,
    captcha_token = solved_token,
    **client.credits.extract_hidden(page.text),
)
```

## Features

- **Async-first** — every request is `async`, backed by `tls_client` on a worker thread for Cloudflare-friendly TLS fingerprints
- **Typed** — `from __future__ import annotations` everywhere, `py.typed` marker, dataclass models for parsed payloads
- **Resource-oriented** — calls grouped under `client.session`, `client.users`, `client.messages`, etc.
- **Typed errors** — full exception hierarchy mapped from HTTP status codes
- **Retries** — exponential backoff on `429` and `5xx`, honors `Retry-After`
- **Proxies** — string-form proxies auto-normalized

## License

MIT