Skip to content
pnxenopoulosPublic

About

Python library to parse, analyze and visualize Counter-Strike 2 data

Topics

Resources

Stars

623 stars

Watchers

8 watching

Forks

Latest commit

 

History

1,470 Commits

Folders and files

Repository files navigation

Awpy

Awpy Discord Docs CI License: MIT

PyPI Downloads Python 3.11+

from awpy import Demo

demo = Demo("match.dem")
demo.load("kills", "damages", "shots")
print(demo.header["map_name"])
print(demo.stats.select("name", "kills", "deaths", "assists", "adr", "kast"))
print(demo.snapshots(seconds=1.0))

Awpy parses Counter-Strike 2 demo files in Rust. Its Python API returns typed Polars DataFrames. Use Polars to filter and join results, write Apache Parquet files, or convert tables to pandas. Use the plotting extra to draw CS2 data with matplotlib.

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.

Table of Contents

Why Awpy?

  • 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.

Install Awpy

Python

Use uv:

uv add awpy

You can also use pip:

pip install awpy

Python 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.

CLI

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.)

Rust library

Add the Awpy Rust crate:

[dependencies]
awpy = "3.1"

Parse CS2 demos with Awpy

Use the Python example above to read a scoreboard and sample player state. See the getting started guide for installation, Polars queries, and map assets.

Multipart demos

Pass files in recording order. Strings and Path objects can be mixed:

from awpy import Demo

demo = Demo(["match-p1.dem", "match-p2.dem"])
print(demo.stats)
state = demo.snapshots(part=1, ticks=5333)

Multipart tables keep each file's ticks and add a zero-based part column. stats and players combine Steam IDs. Explicit tick queries require part. Awpy checks map, build, tick rate, round continuity, and recording boundaries. Splits during live play are not supported. See the multipart guide.

CLI

# 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 --help

Rust

use 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>(())

Select Awpy datasets

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.

Find Awpy workspace crates

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).

Read the Awpy documentation

See awpy.readthedocs.io:

Build and test Awpy

# 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 python

Use 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.

Python tests

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=10

By 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=10

CI 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.

Releases

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 Release Awpy (release.yml) on main. Select awpy-proto, awpy, or awpy-python and enter its Cargo version.

Release in this order: awpy-proto, awpy, then the Python package. The 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. The 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 awpy-python choice 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 the crates.io trusted publishers for release.yml with environment crates, and the PyPI trusted publisher for release.yml with environment pypi. Update existing publishers that refer to the old workflow filenames. Publish each new Rust crate once with an API token before you configure its trusted publisher.

Parsing benchmarks

Run the Criterion suite with a local demo:

AWPY_BENCH_DEMO=/absolute/path/to/match.dem cargo criterion -p awpy --bench parse

See 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.

Regenerating protobufs

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.toml

The 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.

Awpy license

MIT — see LICENSE for details.

About

Python library to parse, analyze and visualize Counter-Strike 2 data

Topics

Resources

Stars

623 stars

Watchers

8 watching

Forks

Contributors

Languages