Lightweight sync and async Python wrapper for the ER:LC PRC API
Project description
erlc-api
Lightweight Python wrapper for the ER:LC PRC API. Version 2 is a breaking,
v2-first release with flat sync and async clients, typed dataclass responses by
default, raw=True escape hatches, flexible commands, and explicit utility
modules that only load when you import them.
Install And Extras
pip install erlc-api
Development install:
pip install -e .[dev]
Optional extras:
| Extra | Installs | Used by |
|---|---|---|
webhooks |
cryptography |
Event webhook Ed25519 signature verification |
export |
openpyxl |
Exporter(...).xlsx(...) |
time |
python-dateutil |
TimeTools().parse(..., enhanced=True) |
rich |
rich |
Formatter().rich_table(...) |
scheduling |
apscheduler |
Advanced scheduling integrations around watchers |
utils |
all utility extras | Export, time, rich, and scheduling helpers |
all |
webhooks plus utility extras | Everything optional |
Example:
pip install "erlc-api[webhooks,export]"
Quickstart
Async apps and bots:
import asyncio
from erlc_api import AsyncERLC, cmd
async def main() -> None:
async with AsyncERLC("server-key") as api:
bundle = await api.server(players=True, queue=True, staff=True)
result = await api.command(cmd.h("Hello from the API"))
print(bundle.name, len(bundle.players or []), result.message)
asyncio.run(main())
Sync scripts:
from erlc_api import ERLC
with ERLC("server-key") as api:
players = api.players()
result = api.command("h Hello")
print(len(players), result.message)
Client Reference
AsyncERLC is for async frameworks, Discord bots, FastAPI apps, background
workers, and anything already running an event loop.
AsyncERLC(
server_key: str | None = None,
*,
global_key: str | None = None,
base_url: str = "https://api.policeroleplay.community",
timeout_s: float = 20.0,
retry_429: bool = True,
user_agent: str | None = None,
)
Use it as an async context manager, or call await api.start() and
await api.close() yourself.
ERLC has the same constructor and method names for sync scripts:
ERLC(
server_key: str | None = None,
*,
global_key: str | None = None,
base_url: str = "https://api.policeroleplay.community",
timeout_s: float = 20.0,
retry_429: bool = True,
user_agent: str | None = None,
)
Every request sends Server-Key. If global_key= is configured, requests also
send Authorization.
Every endpoint method accepts server_key= so one client can work with multiple
servers:
api = ERLC("primary-server-key")
primary = api.players()
secondary = api.players(server_key="secondary-server-key")
validate_key() and health_check() return ValidationResult instead of
raising common API errors.
Endpoint Methods
Typed models are returned by default. Pass raw=True to receive the exact JSON
payload returned by PRC.
| Method | PRC endpoint | Default return type | Notes |
|---|---|---|---|
server(...) |
GET /v2/server |
ServerBundle |
Accepts include flags for v2 sections |
players() |
GET /v2/server?Players=true |
list[Player] |
Parses PlayerName:Id |
staff() |
GET /v2/server?Staff=true |
StaffList |
Staff object maps plus .members() |
queue() |
GET /v2/server?Queue=true |
list[int] |
Queue user IDs in API order |
join_logs() |
GET /v2/server?JoinLogs=true |
list[JoinLogEntry] |
Includes join/leave flag and timestamp |
kill_logs() |
GET /v2/server?KillLogs=true |
list[KillLogEntry] |
Includes killer/victim helpers |
command_logs() |
GET /v2/server?CommandLogs=true |
list[CommandLogEntry] |
Useful with Finder and Analyzer |
mod_calls() |
GET /v2/server?ModCalls=true |
list[ModCallEntry] |
Includes caller/moderator helpers |
emergency_calls() |
GET /v2/server?EmergencyCalls=true |
list[EmergencyCall] |
v2 emergency call payloads |
vehicles() |
GET /v2/server?Vehicles=true |
list[Vehicle] |
Vehicle model, owner, plate, color |
bans() |
GET /v1/server/bans |
BanList |
Uses v1 because v2 does not replace it |
command(command, ...) |
POST /v2/server/command |
CommandResult |
Accepts strings or cmd values |
request(method, path, ...) |
Any path | raw JSON/text | Low-level escape hatch |
server() include options:
bundle = await api.server(players=True, queue=True, staff=True)
everything = await api.server(all=True)
custom = await api.server(include=["players", "vehicles"])
raw_payload = await api.server(all=True, raw=True)
Command API
Commands are intentionally flexible:
from erlc_api import cmd, normalize_command
await api.command(":h hi")
await api.command("h hi")
await api.command(cmd.h("hi"))
await api.command(cmd.pm("Player", "hello"))
await api.command(cmd("pm", "Player", "hello"))
assert normalize_command("h hi") == ":h hi"
Validation is minimal and predictable:
| Rule | Behavior |
|---|---|
| Leading colon missing | Added automatically |
| Blank command | Raises ValueError |
| Newline in command | Raises ValueError |
| Missing command name | Raises ValueError |
:log |
Not blocked by the wrapper |
Dry-run validates and returns a local CommandResult without sending HTTP:
preview = await api.command(cmd.pm("Player", "hello"), dry_run=True)
print(preview.raw["command"], preview.success)
Models
Models are frozen dataclasses. They preserve the original payload in .raw,
unknown fields in .extra, and convert back to dictionaries with .to_dict().
Key models:
| Model | Returned by | Useful fields |
|---|---|---|
ServerInfo |
server() without sections |
name, owner_id, current_players, max_players |
ServerBundle |
server() |
server fields plus optional players, staff, logs, queue, vehicles |
Player |
players() |
player, name, user_id, permission, callsign, team, location |
StaffList |
staff() |
co_owners, admins, mods, helpers, .members() |
CommandLogEntry |
command_logs() |
player, name, user_id, timestamp, command |
CommandResult |
command() |
message, success |
players = await api.players()
first = players[0]
print(first.name, first.user_id)
print(first.extra)
print(first.to_dict())
Parse PRC PlayerName:Id strings directly:
from erlc_api import parse_player_identifier
name, user_id = parse_player_identifier("Avi:123")
Utility Modules
Utilities are explicit lazy modules. import erlc_api only imports clients,
models, errors, and cmd.
| Module | Import | Purpose |
|---|---|---|
| Find | from erlc_api.find import Finder |
Look up players, staff, vehicles, logs, bans, and calls |
| Filter | from erlc_api.filter import Filter |
Chain filters and return .all(), .first(), .count() |
| Sort | from erlc_api.sort import Sorter |
Sort by name, timestamp, team, permission, queue position, vehicle fields |
| Group | from erlc_api.group import Grouper |
Group by team, permission, role, owner, command, day, hour |
| Diff | from erlc_api.diff import Differ |
Compare lists or full server bundles |
| Wait | from erlc_api.wait import AsyncWaiter, Waiter |
Poll until joins, leaves, queue changes, logs, or counts occur |
| Watch | from erlc_api.watch import AsyncWatcher, Watcher |
Stream snapshot diffs as events and callbacks |
| Format | from erlc_api.format import Formatter |
Compact Discord-safe, console-safe, and rich text formatting |
| Analytics | from erlc_api.analytics import Analyzer |
Dashboard summaries, distributions, command usage, moderation trends |
| Export | from erlc_api.export import Exporter |
JSON, CSV, Markdown, HTML, optional XLSX |
| Moderation | from erlc_api.moderation import AsyncModerator, Moderator |
Safe command composition, previews, audit messages |
| Time | from erlc_api.time import TimeTools |
Timestamp parsing, age strings, windows, timezone formatting |
| Schema | from erlc_api.schema import SchemaInspector |
Field discovery, raw/extra inspection, payload diagnostics |
Example:
from erlc_api.find import Finder
from erlc_api.filter import Filter
from erlc_api.export import Exporter
bundle = await api.server(all=True)
player = Finder(bundle).player("Avi")
police = Filter(bundle.players or []).team("Police").all()
csv_text = Exporter(police).csv()
Errors
All wrapper exceptions inherit from ERLCError.
| Exception | Raised when |
|---|---|
APIError |
Non-success response without a more specific mapping |
BadRequestError |
Request payload, path, or params are invalid |
AuthError |
Server key or global key is missing, invalid, banned, or unauthorized |
PermissionDeniedError |
A valid key cannot access the resource |
NotFoundError |
The requested API path/resource was not found |
NetworkError |
Timeout, DNS, connection, or transport failure |
RateLimitError |
PRC returns 429 or a rate-limit error code |
InvalidCommandError |
Command syntax/payload is rejected by PRC |
RestrictedCommandError |
PRC restricts the command from API execution |
ProhibitedMessageError |
Command text is prohibited by PRC |
ServerOfflineError |
Server is offline or unavailable for the request |
RobloxCommunicationError |
PRC cannot communicate with Roblox or the module |
ModuleOutdatedError |
In-game module must be updated |
ModelDecodeError |
Typed decoding received an unexpected payload shape |
from erlc_api import ERLCError, RateLimitError
try:
players = await api.players()
except RateLimitError as exc:
print(exc.retry_after, exc.reset_epoch_s, exc.bucket)
except ERLCError as exc:
print(exc.status_code, exc.error_code, exc.body_excerpt)
Rate Limits
On 429, RateLimitError exposes:
| Attribute | Meaning |
|---|---|
retry_after / retry_after_s |
Seconds to wait when PRC provides Retry-After or body retry data |
reset_epoch_s |
Epoch reset time parsed from X-RateLimit-Reset |
bucket |
Bucket name from X-RateLimit-Bucket |
error_code |
PRC error code when present |
By default retry_429=True, so the transport sleeps once and retries once when
it has retry timing. Set retry_429=False to handle rate limits yourself.
Wiki Deep Dives
The README is the compact API reference. The full GitHub Wiki source lives in
docs/wiki:
- Clients and Authentication
- Endpoint Reference
- Models Reference
- Commands Reference
- Utilities Reference
- Waiters and Watchers
- Formatting, Analytics, and Export
- Moderation Helpers
- Webhooks Reference
- Errors and Rate Limits
- Migration to v2
Development
$env:PYTHONPATH = "src"
python -m pytest -q
python -m ruff check src tests scripts
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file erlc_api_py-2.0.0.tar.gz.
File metadata
- Download URL: erlc_api_py-2.0.0.tar.gz
- Upload date:
- Size: 51.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e31c18a8f8c7d13098b9be9f2eddb2cdc7ad0cda2404e94f395df269b452a963
|
|
| MD5 |
63a6f7d0fcccc5ea95aa3f634836b9e9
|
|
| BLAKE2b-256 |
a2d0a90ef1392cdacff49316db717e30b0dd743784572c2957e52ec4fa4c4d63
|
File details
Details for the file erlc_api_py-2.0.0-py3-none-any.whl.
File metadata
- Download URL: erlc_api_py-2.0.0-py3-none-any.whl
- Upload date:
- Size: 50.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1de02d5c091aa0c0bce2b22a7bf96ef15d0d4a4393094bea7285cc8a306db197
|
|
| MD5 |
5d5e01b4c067d1bcb1df01008ec4aee1
|
|
| BLAKE2b-256 |
e37bd80e7526081296074244fc9c529bf3dc782e6ca57e37246050b74caf7fda
|