Metadata-Version: 2.4
Name: ogu-api
Version: 0.1.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')

        profile = await client.users.get_by_username('forgivenforget')
        print(client.users.extract_username(profile.text))
        print(client.users.extract_credits(profile.text))


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; extract 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       |


## Send a PM

Most authenticated POSTs need a `my_post_key`. Pull it off whatever page you're already loading — here, the compose page:

```python
compose = await client.messages.get_compose_page()

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

## Read the feed

```python
explore = await client.feed.get_explore()

for link in client.feed.extract_thread_links(explore.text)[:5]:
    thread = await client.threads.get_by_link(link)
    print(thread.text[:200])
```

`extract_thread_links` returns paths for both numeric (`/showthread.php?tid=1234`) and slug-rewritten (`/Thread-Some-Slug`) thread URLs. If you specifically need numeric thread IDs, use `extract_thread_ids` — it skips slug-only links.

## 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