Awpy parses Counter-Strike 2 demo files in Rust. Its Python API returns Polars DataFrames.
Awpy and the Deadlock parser Boon use the shared pbdems2 core. Awpy adds CS2 protobufs, field decoders, and name tables. See the PBDEMS2 format guide for the shared format.
- Why Awpy?
- Installation
- Quick Start
- Available Datasets
- Project Structure
- Documentation
- Development
- License
- Read match events, player state, rounds, and statistics as DataFrames.
- Select the datasets, ticks, or entity properties that you need.
- Reconstruct rounds from game state, including demos without round events.
- Use the Python API, Rust library, or command-line interface.
Use uv:
uv add awpyYou can also use pip:
pip install awpyPython 3.11 or later is required. Awpy requires Polars 2.x. Releases include wheels for Python 3.11–3.15 on Linux, macOS, and Windows. Installing a wheel does not require a Rust compiler.
Installing the Python package also installs the awpy command for demo
inspection and map-data management:
awpy info match.dem
awpy kills match.dem --limit 20
awpy stats match.dem --json
awpy get # download the latest map-data release(For parser-internals debugging there is also a Rust developer binary,
awpy-dev: cargo install --path crates/awpy-dev.)
Add the Rust library from the repository:
[dependencies]
awpy = { git = "https://github.com/pnxenopoulos/awpy" }from awpy import Demo
demo = Demo("match.dem")
# File header + playback info
print(demo.header["map_name"]) # "de_inferno"
# Structured datasets, one call each — all Polars DataFrames
rounds = demo.rounds # one row per round, winner + reason
kills = demo.kills # every player_death, participants resolved
damages = demo.damages # every player_hurt, health/armor pre + post
stats = demo.stats # per-player scoreboard (KAST, ADR, ...)
# Any game event, long-form (demo.events.names lists them)
pings = demo.events.player_ping
# Game state at a moment, or sampled across the match
snap = demo.snapshots(ticks=29000) # every player's state at one tick
series = demo.snapshots(every=64) # one sample per 64 ticks
# Per-tick player state — one row per player per tick, decoded in parallel
ticks = demo.ticks() # default: X, Y, Z, health, armor, team_num
ticks = demo.ticks(["health", "m_iTeamNum"])
# Round wins by side
demo.rounds.group_by("winner_side").len()# File header and playback info
awpy info match.dem
# Per-round table (winner, reason, timings)
awpy rounds match.dem
# Kills, with players resolved
awpy kills match.dem --limit 20
# Game events (--summary for counts, a name to dump one event)
awpy events match.dem --summary
awpy events match.dem player_ping --json
# All available commands
awpy --helpuse std::path::Path;
use awpy::Parser;
let parser = Parser::from_file(Path::new("match.dem"))?;
println!("map: {:?}", parser.file_header()?.map_name);
// Game events
for event in parser.events(None)? {
if event.name == "player_death" {
println!("[{}] {:?}", event.tick, event.keys);
}
}
// Entities, tick by tick
parser.run_to_end(|ctx| {
for (_, entity) in ctx.entities.iter() {
if entity.active && entity.class_name == "CCSPlayerPawn" {
// read entity fields
}
}
})?;
# Ok::<(), awpy::Error>(())Dataset properties return cached DataFrames. Use demo.load("kills", "damages", "shots") to collect compatible datasets in one pass. See the dataset reference for columns and selection rules.
| Dataset | Description |
|---|---|
rounds |
Round timings, winners, and end reasons from game state. |
kills |
Kills, participants, positions, and trade flags. |
damages |
Damage events, participants, and health and armor changes. |
bomb |
Bomb actions (pickup, drop, plant start/finish, defuse) with the acting player, position, and bombsite. |
grenades |
Per-tick thrown-grenade trajectories (smoke, HE, flashbang, molotov, decoy). |
fires |
One row per burning inferno (molotov / incendiary), with position, thrower, and [start_tick, end_tick]. |
smokes |
One row per deployed smoke cloud, with position, thrower, and [start_tick, end_tick]. |
shots |
Every weapon_fire event with the shooter's state and active-weapon state (clip, inaccuracy, scoped). |
blinds |
One row per flash event: the thrower, the blinded player (both resolved), and the blind duration. |
item_events |
One row per weapon-item transaction — purchase, pickup, or drop — with the acting player, item, position, and (for buys) cost. |
stats |
Player statistics, including KAST, ADR, trades, clutches, and utility. |
round_economy |
One row per (round, side): team equipment value at freeze end and its buy-type classification (eco / force / full). |
players |
The roster: one row per player seen in the demo (Steam id, name, last side, and team/organization name). |
chat |
Recorded chat messages and channels. |
convars |
Server console variables (mp_maxrounds, ...) as a dict[str, str]. |
snapshots(*, ticks / every / seconds / events / start_tick / end_tick) |
Per-player game state (position, eye angles, health, armor, economy, and loadout) at a tick, a list of ticks, a contiguous range, or sampled across the match by a stride and/or event ticks. |
For events without a dedicated dataset, the demo.events mapping returns any game event as a DataFrame, and ticks(props) returns one row per player per tick for any networked properties.
Use awpy.SNAPSHOT_PROPERTIES to find snapshot fields and their engine properties. awpy.GAME_EVENTS lists common events; demo.events.names lists events in the demo.
| Crate | Description |
|---|---|
awpy |
Core parsing library (io, entity system, demo parser, name tables). |
awpy-dev |
Developer CLI for parser internals (the awpy-dev binary). |
awpy-proto |
Generated CS2 protobuf types (prost). |
awpy-python |
Python bindings via PyO3 + Polars (published as awpy on PyPI). |
See awpy.readthedocs.io:
- Getting Started
- Examples — runnable scripts, also in
crates/awpy-python/examples/ - Datasets
- API Reference
- CLI Reference
- FAQ
- Changelog
# Rust
cargo fmt --all
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo nextest run --workspace --exclude awpy-python
# Python (from crates/awpy-python)
uv sync --locked --no-install-project
uv run --no-sync maturin develop --release
uv run --no-sync pytest -m "not fixtures"
uv run --no-sync ruff check python tests
uv run --no-sync ty check pythonUse Python 3.11–3.14 for source builds with maturin develop.
The release workflow builds a stable-ABI wheel with Python 3.14 for Python 3.15.
The fixtures marker selects all tests that need demo files, including schema,
CLI, and example tests. CI runs these tests in a separate job. Run the offline
tests with pytest -m "not fixtures". Unknown markers and configuration keys
are errors.
For demo-backed tests, fetch the fixture manifest and allow downloads. Run these
commands from crates/awpy-python after the development install:
mkdir -p tests/fixtures
curl -fSL https://raw.githubusercontent.com/pnxenopoulos/awpy-fixtures/main/manifest.json \
-o tests/fixtures/manifest.json
AWPY_RUN_FIXTURES=1 uv run --no-sync pytest -m fixtures --durations=10By default, pytest uses cached demos and skips unavailable demos.
AWPY_RUN_FIXTURES=1 permits downloads and makes missing required demos or an
empty manifest fail the tests.
For a faster run, prepare the files before starting two test workers:
uv run --no-sync python tests/fixture_store.py
AWPY_RUN_FIXTURES=1 AWPY_TICK_SEGMENTS=2 POLARS_MAX_THREADS=2 \
uv run --no-sync pytest -m fixtures -n 2 --dist=loadfile --durations=10CI uses two workers and assigns complete test files to each worker. This keeps
module fixtures shared. Use one worker if memory is limited. Avoid -n auto:
each worker can also start parser and Polars threads.
Fixtures verify each demo file once per worker session. Read-only checks share a parsed demo within a module or parameter; cache and parser-state tests create fresh instances. Do not change the cached DataFrames on a shared demo. Temporary asset caches and monkeypatches remain local to each test.
CI installs dependencies from the lockfile and tests the prebuilt wheel with
uv run --no-sync. This prevents uv from replacing the wheel with a source
build. Keep timing reports enabled when changing fixtures or test scheduling.
Each test checks one behavior. Several assertions can describe the same result. Use named tests for independent results and parameters for input variants. Share expensive, unchanged query results through module fixtures. Keep mutable test doubles, files, and settings local to each test. Remove duplicate checks only when the same behavior and parsing path remain covered. Use exact DataFrame comparisons when the result must match exactly.
Update the package version and finalize the changelog. Push to main and wait
for CI Check to pass on the release commit.
In GitHub Actions, select main, then run:
- Release Parser: select
awpy-protoorawpyand enter its Cargo version. - Release Python: enter the
awpy-pythonversion from the Cargo workspace.
Release in this order: awpy-proto, awpy, then the Python package. Each
workflow checks that the preceding Rust package version is on crates.io.
Skip a Rust package if its version is already published.
Enter the full version without a leading v. Each workflow checks the version,
publishes the package, then creates its tag and GitHub release. Tags use
awpy-proto-v<version>, awpy-v<version>, or awpy-python-v<version>.
Existing tags must refer to the release commit. A retry skips published files
and existing releases. A tag push does not start publication.
The Python workflow publishes a source archive and wheels for Python 3.11–3.15:
- Linux: x86-64 and ARM64.
- macOS: Intel and Apple silicon.
- Windows: x86-64.
Python 3.15 uses a stable-ABI wheel built with Python 3.14, as in CI.
Configure trusted publishers for release-parser.yml with environment crates
and release-python.yml with environment pypi.
Publish each new Rust crate once with an API token before you configure its
trusted publisher.
Run the Criterion suite with a local demo:
AWPY_BENCH_DEMO=/absolute/path/to/match.dem cargo criterion -p awpy --bench parseSee the benchmark guide for runner installation, workload filters, fixed parallelism, and baseline comparisons.
Use the Python API benchmark guide
for isolated timing, peak memory, and output checks through Demo.
crates/awpy-proto/src/proto.rs is generated from the .proto files in
crates/awpy-proto/proto/ (synced from
GameTracking-CS2):
cargo run --locked --manifest-path scripts/build-protos/Cargo.tomlThe generator uses a bundled protoc. It removes unsupported C++ annotations
from temporary compiler inputs. It keeps the upstream .proto files unchanged.
Run its tests with cargo test --locked --manifest-path scripts/build-protos/Cargo.toml.
MIT — see LICENSE for details.