Релиз
This commit is contained in:
58
.gitea/workflows/dev.yaml
Normal file
58
.gitea/workflows/dev.yaml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
name: Verify Dev Build
|
||||||
|
run-name: ${{ github.actor }} verifying dev build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Cache uv binary
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/uv
|
||||||
|
key: uv-${{ runner.os }}
|
||||||
|
restore-keys: uv-${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Cache uv dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/.cache/uv
|
||||||
|
key: uv-${{ runner.os }}
|
||||||
|
restore-keys: uv-${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Cache pre-commit
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pre-commit
|
||||||
|
key: pre-commit-cache-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
restore-keys: pre-commit-cache-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
version: "0.10.5"
|
||||||
|
enable-cache: true
|
||||||
|
cache-local-path: ${{ github.workspace }}/.cache/uv
|
||||||
|
tool-dir: ${{ github.workspace }}/.cache/uv
|
||||||
|
tool-bin-dir: ${{ github.workspace }}/.cache/uv
|
||||||
|
cache-dependency-glob: ""
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
run: uv python install
|
||||||
|
|
||||||
|
- name: Install the project
|
||||||
|
run: uv sync --all-extras --no-install-project --cache-dir ${{ github.workspace }}/.cache/uv
|
||||||
|
|
||||||
|
- name: Linter & Formatter
|
||||||
|
run: uv run pre-commit run --all-files
|
||||||
|
|
||||||
|
- name: Build Package
|
||||||
|
run: uv build --cache-dir ${{ github.workspace }}/.cache/uv
|
||||||
64
.gitea/workflows/latest.yaml
Normal file
64
.gitea/workflows/latest.yaml
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: Build And Publish Package
|
||||||
|
run-name: ${{ github.actor }} builds and publishes package to PyPI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- latest
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Cache uv binary
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/uv
|
||||||
|
key: uv-${{ runner.os }}
|
||||||
|
restore-keys: uv-${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Cache uv dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/.cache/uv
|
||||||
|
key: uv-${{ runner.os }}
|
||||||
|
restore-keys: uv-${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Cache pre-commit
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pre-commit
|
||||||
|
key: pre-commit-cache-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
restore-keys: pre-commit-cache-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
version: "0.10.5"
|
||||||
|
enable-cache: true
|
||||||
|
cache-local-path: ${{ github.workspace }}/.cache/uv
|
||||||
|
tool-dir: ${{ github.workspace }}/.cache/uv
|
||||||
|
tool-bin-dir: ${{ github.workspace }}/.cache/uv
|
||||||
|
cache-dependency-glob: ""
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
run: uv python install
|
||||||
|
|
||||||
|
- name: Install the project
|
||||||
|
run: uv sync --all-extras --no-install-project --cache-dir ${{ github.workspace }}/.cache/uv
|
||||||
|
|
||||||
|
- name: Linter & Formatter
|
||||||
|
run: uv run pre-commit run --all-files
|
||||||
|
|
||||||
|
- name: Build Package
|
||||||
|
run: uv build --cache-dir ${{ github.workspace }}/.cache/uv
|
||||||
|
|
||||||
|
- name: Publish to Gitea PyPI
|
||||||
|
run: |
|
||||||
|
uv publish \
|
||||||
|
--index OxideTwitch \
|
||||||
|
--token ${{ secrets.CI_TOKEN }}
|
||||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# Ruff
|
||||||
|
.ruff_cache
|
||||||
|
|
||||||
|
# uv
|
||||||
|
uv.lock
|
||||||
34
.pre-commit-config.yaml
Normal file
34
.pre-commit-config.yaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/crate-ci/typos
|
||||||
|
rev: v1.42.0
|
||||||
|
hooks:
|
||||||
|
- id: typos
|
||||||
|
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.15.0
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [ --fix ]
|
||||||
|
- id: ruff-format
|
||||||
|
|
||||||
|
- repo: https://github.com/RobertCraigie/pyright-python
|
||||||
|
rev: v1.1.408
|
||||||
|
hooks:
|
||||||
|
- id: pyright
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: check-docstring-first
|
||||||
|
- id: check-added-large-files
|
||||||
|
- id: check-yaml
|
||||||
|
- id: debug-statements
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: double-quote-string-fixer
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
|
||||||
|
- repo: meta
|
||||||
|
hooks:
|
||||||
|
- id: check-hooks-apply
|
||||||
|
- id: check-useless-excludes
|
||||||
96
README.md
Normal file
96
README.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# 🧡 OxideDonationAlerts
|
||||||
|
|
||||||
|
**OxideDonationAlerts** is a high-performance Python client for the [DonationAlerts API](https://www.donationalerts.com/apidoc), built on the [OxideHTTP](https://git.miwory.dev/OxideHTTP/OxideHTTP) core. It leverages Rust-powered HTTP calls to provide a type-safe, rate-limited, and cached interface for managing donations and alerts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔥 Why OxideDonationAlerts?
|
||||||
|
|
||||||
|
Managing real-time donations requires reliability and speed. OxideDonationAlerts simplifies the integration:
|
||||||
|
|
||||||
|
* **⚡ Rust-Powered Core:** Utilizes `pyreqwest` via OxideHTTP for ultra-fast underlying networking.
|
||||||
|
* **🛡️ Distributed Rate Limiting:** Integrates with Redis to manage DonationAlerts' thresholds (60 requests/min) across multiple processes using the GCRA algorithm.
|
||||||
|
* **💾 Smart Caching:** Built-in support for caching API responses (like user profiles or donation history) to reduce latency and API load.
|
||||||
|
* **🏗️ Pydantic Validation:** All responses are fully validated against Pydantic models for perfect type hinting and data integrity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
Configure your private registry in your `uv` environment, then install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv add oxidedonationalerts
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Quick Start
|
||||||
|
|
||||||
|
### Basic Usage: Fetching Donations
|
||||||
|
|
||||||
|
OxideDonationAlerts manages the `base_url` and provides a clean interface for paginated data.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
from oxidedonationalerts.api import DonationAlertsAPIClient
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with DonationAlertsAPIClient(
|
||||||
|
redis_url="redis://localhost:6379"
|
||||||
|
) as client:
|
||||||
|
# Fetch the first page of donations
|
||||||
|
donations = await client.alerts_donations(
|
||||||
|
access_token="your_token",
|
||||||
|
page=1
|
||||||
|
)
|
||||||
|
|
||||||
|
for donation in donations.data:
|
||||||
|
print(f"Donor: {donation.username} | Amount: {donation.amount} {donation.currency}")
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscribing to Real-time Events
|
||||||
|
|
||||||
|
The client supports Centrifuge subscription calls for handling WebSocket-based real-time alerts.
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def setup_realtime(client, token):
|
||||||
|
# Get credentials for WebSocket connection
|
||||||
|
channels = ["$alerts:donation_741", "$goals:goal_123"]
|
||||||
|
subs = await client.centrifuge_subscribe(
|
||||||
|
access_token=token,
|
||||||
|
client="client_uuid_from_frontend",
|
||||||
|
subscriptions=channels
|
||||||
|
)
|
||||||
|
print(f"Subscription Token: {subs.channels[0].token}")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Advanced: Configuration
|
||||||
|
|
||||||
|
Ensure your `pyproject.toml` points to the correct registry for both **OxideDonationAlerts** and its core dependency **OxideHTTP**:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[tool.uv.index]]
|
||||||
|
name = "OxideHTTP"
|
||||||
|
url = "https://git.miwory.dev/api/packages/OxideHTTP/pypi/simple"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Implementation Status
|
||||||
|
|
||||||
|
The client is currently optimized for the following scopes:
|
||||||
|
|
||||||
|
* **User Data:** `oauth-user-show`
|
||||||
|
* **Donations:** `oauth-donation-index`
|
||||||
|
* **Real-time:** `oauth-centrifuge-subscribe`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Would you like me to generate the **Pydantic schemas** for the `UserOauth` or `AlertsDonations` models to match the official documentation?
|
||||||
101
pyproject.toml
Normal file
101
pyproject.toml
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
[project]
|
||||||
|
name = "oxidedonationalerts"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Client for DonationAlerts API"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [{ name = "Miwory", email = "miwory.uwu@gmail.com" }]
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = ["oxidehttp>=1.0.3,<=2.0.0", "pydantic[email]>=2.12,<=2.13"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.9.2,<0.10.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"ty>=0.0.17",
|
||||||
|
"ruff>=0.15.0",
|
||||||
|
"pyright>=1.1.408",
|
||||||
|
"poethepoet>=0.40.0",
|
||||||
|
"pre-commit>=4.5.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
name = "OxideHTTP"
|
||||||
|
url = "https://git.miwory.dev/api/packages/OxideHTTP/pypi/simple"
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
name = "OxideDonationAlerts"
|
||||||
|
url = "https://git.miwory.dev/api/packages/OxideHTTP/pypi/simple"
|
||||||
|
publish-url = "https://git.miwory.dev/api/packages/OxideHTTP/pypi"
|
||||||
|
explicit = true
|
||||||
|
|
||||||
|
[tool.poe.tasks]
|
||||||
|
_git = "git add ."
|
||||||
|
_lint = "pre-commit run --all-files"
|
||||||
|
|
||||||
|
lint = ["_git", "_lint"]
|
||||||
|
check = "uv pip ls --outdated"
|
||||||
|
|
||||||
|
major = "uv version --bump major"
|
||||||
|
minor = "uv version --bump minor"
|
||||||
|
patch = "uv version --bump patch"
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
venvPath = "."
|
||||||
|
venv = ".venv"
|
||||||
|
strictListInference = true
|
||||||
|
strictDictionaryInference = true
|
||||||
|
strictSetInference = true
|
||||||
|
deprecateTypingAliases = true
|
||||||
|
typeCheckingMode = "strict"
|
||||||
|
pythonPlatform = "All"
|
||||||
|
stubPath = "typings"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py313"
|
||||||
|
line-length = 79
|
||||||
|
fix = true
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
preview = true
|
||||||
|
select = [
|
||||||
|
"E",
|
||||||
|
"W",
|
||||||
|
"F",
|
||||||
|
"UP",
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"C4",
|
||||||
|
"SIM",
|
||||||
|
"I",
|
||||||
|
"S",
|
||||||
|
"G",
|
||||||
|
"FAST",
|
||||||
|
"ASYNC",
|
||||||
|
"BLE",
|
||||||
|
"INT",
|
||||||
|
"ISC",
|
||||||
|
"ICN",
|
||||||
|
"PYI",
|
||||||
|
"INP",
|
||||||
|
"RSE",
|
||||||
|
"PIE",
|
||||||
|
"SLOT",
|
||||||
|
"TID",
|
||||||
|
"LOG",
|
||||||
|
"FBT",
|
||||||
|
"DTZ",
|
||||||
|
"EM",
|
||||||
|
"PERF",
|
||||||
|
"RUF",
|
||||||
|
]
|
||||||
|
ignore = ["RUF002", "RUF029", "S101", "S104", "W505"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydoclint]
|
||||||
|
ignore-one-line-docstrings = true
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "single"
|
||||||
|
indent-style = "space"
|
||||||
|
docstring-code-format = true
|
||||||
0
src/oxidedonationalerts/__init__.py
Normal file
0
src/oxidedonationalerts/__init__.py
Normal file
145
src/oxidedonationalerts/api.py
Normal file
145
src/oxidedonationalerts/api.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
from typing import Literal, Never
|
||||||
|
|
||||||
|
from oxidehttp.client import OxideHTTP
|
||||||
|
from oxidehttp.schema import CachedResponse, Response
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
|
||||||
|
|
||||||
|
class DonationAlertsAPIClient(OxideHTTP):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
redis_url: str | None = None,
|
||||||
|
proxy_url: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.base_uri = 'https://www.donationalerts.com/api/v1'
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
base_url=self.base_uri,
|
||||||
|
redis_url=redis_url,
|
||||||
|
ratelimit_key='donation_alerts' if redis_url else None,
|
||||||
|
ratelimit_limit=60 if redis_url else None,
|
||||||
|
proxy_url=proxy_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _auth(self, access_token: str) -> dict[str, str]:
|
||||||
|
return {'Authorization': f'Bearer {access_token}'}
|
||||||
|
|
||||||
|
async def _process[T: BaseModel](
|
||||||
|
self, req: Response | CachedResponse, schema: type[T]
|
||||||
|
) -> T:
|
||||||
|
if req.status_code >= 500:
|
||||||
|
raise s.InternalError(req.status_code, 'Internal Server Error')
|
||||||
|
|
||||||
|
if req.status_code >= 400 and req.status_code < 500:
|
||||||
|
data = await req.json()
|
||||||
|
message = data.get('message', 'DonationAlerts API Error')
|
||||||
|
|
||||||
|
raise s.ClientError(req.status_code, message)
|
||||||
|
|
||||||
|
data = await req.json()
|
||||||
|
return schema.model_validate(data)
|
||||||
|
|
||||||
|
async def user_oauth(
|
||||||
|
self, access_token: str, *, cache_ttl: int | None = None
|
||||||
|
) -> s.UserOauth:
|
||||||
|
req = await self.get(
|
||||||
|
'/user/oauth', None, self._auth(access_token), cache_ttl
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self._process(req, s.UserOauth)
|
||||||
|
|
||||||
|
async def alerts_donations(
|
||||||
|
self, access_token: str, *, page: int = 1, cache_ttl: int | None = None
|
||||||
|
) -> s.AlertsDonations:
|
||||||
|
req = await self.get(
|
||||||
|
'/alerts/donations',
|
||||||
|
{'page': page},
|
||||||
|
self._auth(access_token),
|
||||||
|
cache_ttl,
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self._process(req, s.AlertsDonations)
|
||||||
|
|
||||||
|
async def custom_alert(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
external_id: str,
|
||||||
|
header: str,
|
||||||
|
message: str,
|
||||||
|
image_url: str,
|
||||||
|
sound_url: str,
|
||||||
|
is_shown: Literal[0, 1] = 0,
|
||||||
|
) -> Never:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def merchandise(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
merchant_identifier: str,
|
||||||
|
merchandise_identifier: str,
|
||||||
|
title: dict[str, str],
|
||||||
|
currency: str,
|
||||||
|
price_user: int,
|
||||||
|
price_service: str,
|
||||||
|
url: str,
|
||||||
|
img_url: str,
|
||||||
|
end_at_ts: int,
|
||||||
|
is_active: Literal[0, 1] = 0,
|
||||||
|
is_percentage: Literal[0, 1] = 0,
|
||||||
|
) -> Never:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def update_merchandise(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
merchant_identifier: str,
|
||||||
|
merchandise_identifier: str,
|
||||||
|
title: dict[str, str],
|
||||||
|
currency: str,
|
||||||
|
price_user: int,
|
||||||
|
price_service: str,
|
||||||
|
url: str,
|
||||||
|
img_url: str,
|
||||||
|
end_at_ts: int,
|
||||||
|
is_active: Literal[0, 1] = 0,
|
||||||
|
is_percentage: Literal[0, 1] = 0,
|
||||||
|
) -> Never:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def get_user_data_from_promocode(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
promocode: str,
|
||||||
|
*,
|
||||||
|
cache_ttl: int | None = None,
|
||||||
|
) -> Never:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def send_sale_alerts(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
user_id: int,
|
||||||
|
external_id: str,
|
||||||
|
merchant_identifier: str,
|
||||||
|
merchandise_identifier: str,
|
||||||
|
amount: float,
|
||||||
|
currency: str,
|
||||||
|
username: str,
|
||||||
|
message: str,
|
||||||
|
bought_amount: int = 1,
|
||||||
|
) -> Never:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def centrifuge_subscribe(
|
||||||
|
self, access_token: str, client: str, subscriptions: list[str]
|
||||||
|
) -> s.CentrifugeSubscribe:
|
||||||
|
req = await self.post(
|
||||||
|
'/centrifuge/subscribe',
|
||||||
|
None,
|
||||||
|
self._auth(access_token),
|
||||||
|
{'client': client, 'channels': subscriptions},
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self._process(req, s.CentrifugeSubscribe)
|
||||||
78
src/oxidedonationalerts/auth.py
Normal file
78
src/oxidedonationalerts/auth.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
from oxidehttp.client import OxideHTTP
|
||||||
|
from oxidehttp.schema import CachedResponse, Response
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
from . import scopes as sp
|
||||||
|
|
||||||
|
|
||||||
|
class DonationAlertsAuthClient(OxideHTTP):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client_id: str,
|
||||||
|
client_secret: str,
|
||||||
|
redirect_uri: str,
|
||||||
|
redis_url: str | None = None,
|
||||||
|
proxy_url: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.base_uri = 'https://www.donationalerts.com/oauth'
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self.redirect_uri = redirect_uri
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
base_url=self.base_uri,
|
||||||
|
redis_url=redis_url,
|
||||||
|
ratelimit_key='donation_alerts' if redis_url else None,
|
||||||
|
ratelimit_limit=60 if redis_url else None,
|
||||||
|
proxy_url=proxy_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _auth(self, access_token: str) -> dict[str, str]:
|
||||||
|
return {'Authorization': f'Bearer {access_token}'}
|
||||||
|
|
||||||
|
async def _process[T: BaseModel](
|
||||||
|
self, req: Response | CachedResponse, schema: type[T]
|
||||||
|
) -> T:
|
||||||
|
if req.status_code >= 500:
|
||||||
|
raise s.InternalError(req.status_code, 'Internal Server Error')
|
||||||
|
|
||||||
|
if req.status_code >= 400 and req.status_code < 500:
|
||||||
|
data = await req.json()
|
||||||
|
message = data.get('message', 'DonationAlerts API Error')
|
||||||
|
|
||||||
|
raise s.ClientError(req.status_code, message)
|
||||||
|
|
||||||
|
data = await req.json()
|
||||||
|
return schema.model_validate(data)
|
||||||
|
|
||||||
|
async def user_access_token(self, code: str) -> s.UserAccessToken:
|
||||||
|
req = await self.post(
|
||||||
|
'/token',
|
||||||
|
json={
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self._process(req, s.UserAccessToken)
|
||||||
|
|
||||||
|
async def refresh_access_token(
|
||||||
|
self, refresh_code: str, scopes: list[sp.Any]
|
||||||
|
) -> s.UserAccessToken:
|
||||||
|
req = await self.post(
|
||||||
|
'/token',
|
||||||
|
json={
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'refresh_token': refresh_code,
|
||||||
|
'scopes': ' '.join(scopes),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self._process(req, s.UserAccessToken)
|
||||||
0
src/oxidedonationalerts/py.typed
Normal file
0
src/oxidedonationalerts/py.typed
Normal file
115
src/oxidedonationalerts/schema.py
Normal file
115
src/oxidedonationalerts/schema.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field, HttpUrl
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
status_code: int
|
||||||
|
error: str
|
||||||
|
|
||||||
|
def __init__(self, status_code: int, error: str) -> None:
|
||||||
|
self.status_code = status_code
|
||||||
|
self.error = error
|
||||||
|
super().__init__(f'{status_code}: {error}')
|
||||||
|
|
||||||
|
|
||||||
|
class ClientError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InternalError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSchema(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid')
|
||||||
|
|
||||||
|
|
||||||
|
class UserOauthData(BaseSchema):
|
||||||
|
id: int
|
||||||
|
code: str
|
||||||
|
name: str
|
||||||
|
avatar: HttpUrl
|
||||||
|
email: EmailStr
|
||||||
|
socket_connection_token: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserOauth(BaseSchema):
|
||||||
|
data: UserOauthData
|
||||||
|
|
||||||
|
|
||||||
|
class AlertsDonationsDataPayinSystem(BaseSchema):
|
||||||
|
title: str
|
||||||
|
|
||||||
|
|
||||||
|
class AlertsDonationsDataRecipient(BaseSchema):
|
||||||
|
user_id: int
|
||||||
|
code: str
|
||||||
|
name: str
|
||||||
|
avatar: HttpUrl
|
||||||
|
|
||||||
|
|
||||||
|
class AlertsDonationsData(BaseSchema):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
username: str | None
|
||||||
|
message: str | None
|
||||||
|
message_type: Literal['text', 'audio']
|
||||||
|
payin_system: AlertsDonationsDataPayinSystem | None
|
||||||
|
amount: float
|
||||||
|
currency: str
|
||||||
|
is_shown: bool
|
||||||
|
amount_in_user_currency: float
|
||||||
|
recipient_name: str
|
||||||
|
recipient: AlertsDonationsDataRecipient
|
||||||
|
created_at: datetime
|
||||||
|
created_at_ts: int
|
||||||
|
shown_at: datetime | None
|
||||||
|
shown_at_ts: int | None
|
||||||
|
|
||||||
|
|
||||||
|
class AlertsDonationsLinks(BaseSchema):
|
||||||
|
first: HttpUrl
|
||||||
|
last: HttpUrl
|
||||||
|
prev: HttpUrl | None
|
||||||
|
next: HttpUrl | None
|
||||||
|
|
||||||
|
|
||||||
|
class MetaLink(BaseSchema):
|
||||||
|
url: HttpUrl | None
|
||||||
|
label: str
|
||||||
|
active: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AlertsDonationsMeta(BaseSchema):
|
||||||
|
current_page: int
|
||||||
|
from_page: int | None = Field(alias='from')
|
||||||
|
last_page: int
|
||||||
|
links: list[MetaLink]
|
||||||
|
path: HttpUrl
|
||||||
|
per_page: int
|
||||||
|
to: int | None
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
class AlertsDonations(BaseSchema):
|
||||||
|
data: list[AlertsDonationsData]
|
||||||
|
links: AlertsDonationsLinks
|
||||||
|
meta: AlertsDonationsMeta
|
||||||
|
|
||||||
|
|
||||||
|
class CentrifugeSubscribeChannel(BaseSchema):
|
||||||
|
channel: str
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class CentrifugeSubscribe(BaseSchema):
|
||||||
|
channels: list[CentrifugeSubscribeChannel]
|
||||||
|
|
||||||
|
|
||||||
|
class UserAccessToken(BaseSchema):
|
||||||
|
token_type: Literal['Bearer']
|
||||||
|
access_token: str
|
||||||
|
refresh_token: str
|
||||||
|
expires_in: int
|
||||||
18
src/oxidedonationalerts/scopes.py
Normal file
18
src/oxidedonationalerts/scopes.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
OAUTH_USER_SHOW = 'oauth-user-show'
|
||||||
|
OAUTH_DONATION_SUBSCRIBE = 'oauth-donation-subscribe'
|
||||||
|
OAUTH_DONATION_INDEX = 'oauth-donation-index'
|
||||||
|
OAUTH_CUSTOM_ALERT_STORE = 'oauth-custom_alert-store'
|
||||||
|
OAUTH_GOAL_SUBSCRIBE = 'oauth-goal-subscribe'
|
||||||
|
OAUTH_POLL_SUBSCRIBE = 'oauth-poll-subscribe'
|
||||||
|
|
||||||
|
|
||||||
|
type Any = Literal[
|
||||||
|
'oauth-user-show',
|
||||||
|
'oauth-donation-subscribe',
|
||||||
|
'oauth-donation-index',
|
||||||
|
'oauth-custom_alert-store',
|
||||||
|
'oauth-goal-subscribe',
|
||||||
|
'oauth-poll-subscribe',
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user