Lightweight sync and async Python wrapper for the ER:LC PRC API
Project description
erlc-api.py
Lightweight Python wrapper for the ER:LC PRC API. Version 2 is a v2-first release with flat sync and async clients, typed dataclass responses by default, safe rate-limit handling, flexible commands, and explicit utility modules that only load when you import them.
Install the PyPI package as erlc-api.py; import it in Python as erlc_api.
Install And Extras
pip install erlc-api.py
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 |
location |
Pillow |
Optional map overlays through MapRenderer |
utils |
all utility extras | Export, time, rich, scheduling, and location helpers |
all |
webhooks plus utility extras | Everything optional |
Example:
pip install "erlc-api.py[webhooks,export]"
Package Name And Import Name
| Where | Name |
|---|---|
| PyPI install | pip install erlc-api.py |
| Python import | import erlc_api |
| Core imports | from erlc_api import AsyncERLC, ERLC, cmd |
The repository URL still uses erlc-api, but the published package name is
erlc-api.py to avoid ambiguity with other packages.
Quickstart
Async apps and bots:
import asyncio
from erlc_api import AsyncERLC, CommandPolicy, cmd
async def main() -> None:
policy = CommandPolicy(allowed={"h"}, max_length=120)
async with AsyncERLC("server-key") as api:
bundle = await api.server(players=True, queue=True, staff=True)
preview = await api.command(policy.validate(cmd.h("Hello from the API")), dry_run=True)
print(bundle.name, len(bundle.players or []), preview.raw["command"])
asyncio.run(main())
Sync scripts:
from erlc_api import ERLC, CommandPolicy
with ERLC("server-key") as api:
policy = CommandPolicy(allowed={"h"}, max_length=120)
players = api.players()
result = api.command(policy.validate("h Hello"), dry_run=True)
print(len(players), result.message)
Safe Defaults
- Dynamic process-local rate limiting is enabled by default with
rate_limited=True. retry_429=Truesleeps once and retries once when PRC provides retry timing.- Commands stay flexible, but bot/web examples should gate execution with
CommandPolicy, permissions, and cooldowns. - Server keys are never stored or encrypted by the wrapper; keep them in your secret manager or environment.
from erlc_api.security import key_fingerprint
print(key_fingerprint("server-key")) # safe for logs; never log the key itself
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,
rate_limited: 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,
rate_limited: 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 when you need raw PRC data;
the exact shape depends on the method and is summarized below.
| 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 |
Endpoint Version Map
| API area | PRC version | Wrapper methods |
|---|---|---|
| Server status and includes | v2 | server, players, staff, queue, logs, vehicles, emergency_calls |
| Command execution | v2 | command |
| Bans | v1 | bans |
| Custom requests | caller chooses | request |
Support Matrix
| Feature | Built in | Notes |
|---|---|---|
| Async client | Yes | AsyncERLC |
| Sync client | Yes | ERLC |
| Typed dataclasses | Yes | Default response mode |
| Raw PRC data | Yes | raw=True |
| Dynamic rate limiting | Yes | Enabled by default, process-local |
| Event webhook verification | Optional extra | erlc-api.py[webhooks] |
| Discord bot framework | No | Docs use discord.py; wrapper stays framework-neutral |
| Persistent/distributed cache | No | Bring your own adapter or external store |
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 CommandPolicy, CommandPolicyError, 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)
For Discord bots, web routes, and custom-command handlers, put an application policy in front of command execution:
policy = CommandPolicy(allowed={"h", "pm"}, max_length=120)
try:
safe_command = policy.validate(cmd.h("Short staff announcement"))
except CommandPolicyError as exc:
print(exc.result.reason)
else:
await api.command(safe_command)
CommandPolicy.check(...) returns a CommandPolicyResult for previews and UI;
CommandPolicy.validate(...) raises CommandPolicyError when blocked.
Raw Response Behavior
raw=True returns PRC data before typed model decoding, but wrapper convenience
methods intentionally return the section they are named after:
| Call | raw=True returns |
|---|---|
api.server(raw=True) |
Full /v2/server payload |
api.server(players=True, raw=True) |
Full /v2/server payload including Players |
api.players(raw=True) |
Raw Players list only |
api.staff(raw=True) |
Raw Staff object only |
api.queue(raw=True) |
Raw Queue list only |
log/vehicle/call helpers with raw=True |
Raw section list only |
api.bans(raw=True) |
Full raw v1 bans mapping |
api.command(raw=True) |
Raw v2 command response |
api.request(...) |
Raw decoded response body |
Model .to_dict() output uses wrapper field names and helper shapes. It is
JSON-safe, but it is not guaranteed to be byte-for-byte identical to PRC JSON.
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 |
| Snapshot | from erlc_api.snapshot import SnapshotStore |
JSONL snapshot persistence and latest-state comparisons |
| Audit | from erlc_api.audit import AuditLog |
JSON-safe audit events for commands, webhooks, watchers, and moderation |
| Idempotency | from erlc_api.idempotency import MemoryDeduper, FileDeduper |
TTL dedupe for webhook deliveries and watcher restarts |
| Limits | from erlc_api.limits import poll_plan, safe_interval |
Conservative polling guidance without fake PRC limit claims |
| Rate Limit | from erlc_api.ratelimit import AsyncRateLimiter, RateLimiter |
Dynamic in-memory limiter used by clients by default |
| Error Codes | from erlc_api.error_codes import explain_error_code |
Explain PRC error codes and wrapper exception mappings |
| Custom Commands | from erlc_api.custom_commands import CustomCommandRouter |
Framework-neutral router for PRC webhook messages starting with ; |
| Location | from erlc_api.location import LocationTools |
Distances, nearest players, postal/street matching, map URLs, optional overlays |
| Bundle | from erlc_api.bundle import AsyncBundle, Bundle |
Named/custom v2 bundle presets without changing the client |
| Rules | from erlc_api.rules import RuleEngine, Conditions |
Evaluate flexible alert rules and return matches/callback results |
| Multi Server | from erlc_api.multiserver import AsyncMultiServer, MultiServer |
Read and aggregate multiple named servers with bounded concurrency |
| Discord Tools | from erlc_api.discord_tools import DiscordFormatter |
Dependency-free Discord embed/message payload dictionaries |
| Diagnostics | from erlc_api.diagnostics import diagnose_error |
User-facing diagnostics from errors, rate limits, command results, and status |
| Cache | from erlc_api.cache import AsyncCachedClient, CachedClient |
Explicit memory TTL caching for read endpoints plus adapter protocols |
| Status | from erlc_api.status import AsyncStatus, StatusBuilder |
Typed dashboard status snapshots with .to_dict() |
| Command Flows | from erlc_api.command_flows import CommandFlowBuilder |
Preview and validate command sequences without executing them |
Example:
from erlc_api.find import Finder
from erlc_api.filter import Filter
from erlc_api.export import Exporter
from erlc_api.snapshot import SnapshotStore
from erlc_api.bundle import AsyncBundle
from erlc_api.status import StatusBuilder
bundle = await AsyncBundle(api).dashboard()
player = Finder(bundle).player("Avi")
police = Filter(bundle.players or []).team("Police").all()
csv_text = Exporter(police).csv()
SnapshotStore("snapshots.jsonl").save(bundle)
status = StatusBuilder(bundle).build()
Custom in-game commands are received through PRC Event Webhooks. Use
erlc_api.webhooks for signature verification and erlc_api.custom_commands
for flexible routing:
from erlc_api.custom_commands import CustomCommandRouter
router = CustomCommandRouter(prefix=";")
@router.command("ping", "p")
async def ping(ctx):
return ctx.reply("pong")
result = await router.dispatch({"Message": ";p"})
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.
Dynamic rate limiting is enabled by default. It learns from PRC rate-limit headers and waits before avoidable requests:
api = AsyncERLC("server-key")
print(api.rate_limits)
Pass rate_limited=False only when your application has its own limiter and
you want to disable the wrapper's pre-request waiting.
Known Limitations
- Built-in rate limiting is process-local. Multiple bot shards, containers, or workers need external coordination if they share keys.
- The wrapper does not store, encrypt, rotate, or validate server keys unless
your app calls
validate_key(). - Command execution is powerful. Gate it with Discord permissions, web auth,
cooldowns,
CommandPolicy, dry-run previews, and audit logs. - Discord and FastAPI examples are safe templates, not complete production bot or web security systems.
- Optional rendering/export/webhook features load only when explicitly imported and installed through extras.
Documentation Deep Dives
The README is the compact API reference. The full documentation source lives in
docs/wiki:
- Home
- Installation and Extras
- Getting Started
- Quickstart: Web Backend
- Quickstart: Discord.py
- FAQ
- Clients and Authentication
- Endpoint Reference
- Endpoint Usage Cookbook
- Models Reference
- Typed vs Raw Responses
- Commands Reference
- Function List
- Utilities Reference
- Ops Utilities Reference
- Workflow Utilities Reference
- Formatting, Analytics, and Export
- Moderation Helpers
- Waiters and Watchers
- Webhooks Reference
- Event Webhooks and Custom Commands
- Custom Commands Reference
- Security and Secrets
- Rate Limits, Retries, and Reliability
- Errors and Rate Limits
- Error Handling and Troubleshooting
- Testing and Mocking
- Migration to v2
- Comparison and Why erlc-api.py
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.3.1.tar.gz.
File metadata
- Download URL: erlc_api_py-2.3.1.tar.gz
- Upload date:
- Size: 88.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2cddee500f75774ee614c39dd60e524ec45ddae5bfc9fdcc89d04499d0618267
|
|
| MD5 |
5bd2af72045060fde58c3ca3dc092508
|
|
| BLAKE2b-256 |
bb7202caa6344544af3fe28c2f54d11d210d9c331dd7a7b2f2e36b572b3733c4
|
File details
Details for the file erlc_api_py-2.3.1-py3-none-any.whl.
File metadata
- Download URL: erlc_api_py-2.3.1-py3-none-any.whl
- Upload date:
- Size: 85.0 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 |
609634e699e67369b4c5fefe12532c63a4808798939c39586a68e3e4b80b8c29
|
|
| MD5 |
dab7967bd3a386292ea55776fa5f2fa2
|
|
| BLAKE2b-256 |
ba6f8fd5024deb00fc31871aa67be036ee5ebc8c9a1901a2aabecba615ff1cfa
|