Skip to main content

Lightweight sync and async Python wrapper for the ER:LC PRC API

Project description

erlc-api.py

PyPI Python License

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=True sleeps 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:

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

erlc_api_py-2.3.1.tar.gz (88.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

erlc_api_py-2.3.1-py3-none-any.whl (85.0 kB view details)

Uploaded Python 3

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

Hashes for erlc_api_py-2.3.1.tar.gz
Algorithm Hash digest
SHA256 2cddee500f75774ee614c39dd60e524ec45ddae5bfc9fdcc89d04499d0618267
MD5 5bd2af72045060fde58c3ca3dc092508
BLAKE2b-256 bb7202caa6344544af3fe28c2f54d11d210d9c331dd7a7b2f2e36b572b3733c4

See more details on using hashes here.

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

Hashes for erlc_api_py-2.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 609634e699e67369b4c5fefe12532c63a4808798939c39586a68e3e4b80b8c29
MD5 dab7967bd3a386292ea55776fa5f2fa2
BLAKE2b-256 ba6f8fd5024deb00fc31871aa67be036ee5ebc8c9a1901a2aabecba615ff1cfa

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page