Skip to content

Commit

Permalink
better pydantic model
Browse files Browse the repository at this point in the history
  • Loading branch information
pnxenopoulos committed Apr 3, 2024
1 parent f1f2f4d commit 1e14d66
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 85 deletions.
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ To install Awpy, you can run
pip install awpy
```

`awpy` requires [Python](https://www.python.org/downloads/) >= 3.11. To update the library, just run `pip install --upgrade awpy`. To check your current version, run `pip freeze | grep awpy`.
`awpy` requires [Python](https://www.python.org/downloads/) >= 3.9. To update the library, just run `pip install --upgrade awpy`. To check your current version, run `pip freeze | grep awpy`.

:bulb: **Tip:** Don't worry if you get stuck, visit us [our Discord](https://discord.gg/W34XjsSs2H) for help.

Expand Down Expand Up @@ -66,13 +66,16 @@ Awpy is structured as follows:
```
.
├── awpy
│   ├── data # Code for dealing with Counter-Strike map and nav data
│   ├── parser # Code for Counter-Strike demo parser
│   ├── stats # Code for Counter-Strike statistics and analytics
│   └── visualization # Code for Counter-Strike visualization
├── doc # Contains documentation files
├── examples # Contains Jupyter Notebooks showing example code
└── tests # Contains tests for the awpy package
│   ├── data # Data directory (PLANNED)
│   ├── stats # Stats and analytics module
│   └── visualization # Visualization module (PLANNED)
│   converters.py # Utilities for converting to readable strings
│   demo.py # Defines the base Demo class
│   parsers.py # Defines simple parsers for different events
│   utils.py # Utilities used across the project
├── doc # Documentation files
├── examples # Jupyter Notebooks showing example code
└── tests # Tests
```

## Acknowledgments
Expand Down
150 changes: 75 additions & 75 deletions awpy/demo.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""Defines the Demo class."""

import os
from typing import Any
from typing import Any, Optional

import pandas as pd
from demoparser2 import DemoParser # pylint: disable=E0611
from pydantic import BaseModel, ConfigDict, model_validator
from pydantic import BaseModel, ConfigDict, Field, FilePath

from awpy.parsers import (
parse_bomb,
Expand Down Expand Up @@ -43,51 +42,42 @@ class Demo(BaseModel): # pylint: disable=too-many-instance-attributes

model_config = ConfigDict(arbitrary_types_allowed=True)

file: FilePath

# Parser & Metadata
file: str
parser: DemoParser
header: DemoHeader
events: dict[str, pd.DataFrame]
parser: DemoParser = Field(default=None)
header: DemoHeader = Field(default=None)
events: dict[str, pd.DataFrame] = Field(default=dict)

# Data
kills: pd.DataFrame
damages: pd.DataFrame
bomb: pd.DataFrame
smokes: pd.DataFrame
infernos: pd.DataFrame
weapon_fires: pd.DataFrame
rounds: pd.DataFrame
grenades: pd.DataFrame
ticks: pd.DataFrame

@model_validator(mode="before")
@classmethod
def parse_demo(cls: type["Demo"], values: dict[str, Any]) -> dict[str, Any]:
"""Parse the demo file.
Args:
values (dict[str, Any]): Passed in arguments.
Raises:
ValueError: If `file` is not a passed argument.
FileNotFoundError: If specified filepath does not exist.
Returns:
dict[str, Any]: The parsed demo data.
"""
file = values.get("file")
if file is None:
file_arg_error_msg = "Must specify filepath with `file` argument."
raise ValueError(file_arg_error_msg)
if not os.path.exists(file):
file_not_found_error_msg = f"{file} not found."
raise FileNotFoundError(file_not_found_error_msg)

parser = DemoParser(file)
header = parse_header(parser.parse_header())
events = dict(
parser.parse_events(
parser.list_game_events(),
kills: Optional[pd.DataFrame] = Field(default=None)
damages: Optional[pd.DataFrame] = Field(default=None)
bomb: Optional[pd.DataFrame] = Field(default=None)
smokes: Optional[pd.DataFrame] = Field(default=None)
infernos: Optional[pd.DataFrame] = Field(default=None)
weapon_fires: Optional[pd.DataFrame] = Field(default=None)
rounds: Optional[pd.DataFrame] = Field(default=None)
grenades: Optional[pd.DataFrame] = Field(default=None)
ticks: Optional[pd.DataFrame] = Field(default=None)

def __init__(self, **data: dict[str, Any]) -> None:
"""Initialize the Demo class. Performs any parsing."""
super().__init__(**data)

self.parser = DemoParser(str(self.file))
self._parse_demo()
self._parse_events()

def _parse_demo(self) -> None:
"""Parse the demo header and file."""
if not self.parser:
no_parser_error_msg = "No parser found!"
raise ValueError(no_parser_error_msg)

self.header = parse_header(self.parser.parse_header())
self.events = dict(
self.parser.parse_events(
self.parser.list_game_events(),
player=[
"X",
"Y",
Expand Down Expand Up @@ -127,38 +117,48 @@ def parse_demo(cls: type["Demo"], values: dict[str, Any]) -> dict[str, Any]:
)
)

# Parse the demo
rounds = parse_rounds(parser)
def _parse_events(self) -> None:
"""Process the raw parsed data."""
if len(self.events.keys()) == 0:
no_events_error_msg = "No events found!"
raise ValueError(no_events_error_msg)

self.rounds = parse_rounds(self.parser)

kills = apply_round_num(rounds, parse_kills(events))
damages = apply_round_num(rounds, parse_damages(events))
bomb = apply_round_num(rounds, parse_bomb(events))
smokes = apply_round_num(rounds, parse_smokes(events), tick_col="start_tick")
infernos = apply_round_num(
rounds, parse_infernos(events), tick_col="start_tick"
self.kills = apply_round_num(self.rounds, parse_kills(self.events))
self.damages = apply_round_num(self.rounds, parse_damages(self.events))
self.bomb = apply_round_num(self.rounds, parse_bomb(self.events))
self.smokes = apply_round_num(
self.rounds, parse_smokes(self.events), tick_col="start_tick"
)
self.infernos = apply_round_num(
self.rounds, parse_infernos(self.events), tick_col="start_tick"
)
self.weapon_fires = apply_round_num(
self.rounds, parse_weapon_fires(self.events)
)
self.grenades = apply_round_num(self.rounds, parse_grenades(self.parser))
self.ticks = apply_round_num(self.rounds, parse_ticks(self.parser))

@property
def is_parsed(self) -> bool:
"""Check if the demo has been parsed."""
return all(
[
self.parser,
self.header,
self.events,
self.kills is not None,
self.damages is not None,
self.bomb is not None,
self.smokes is not None,
self.infernos is not None,
self.weapon_fires is not None,
self.rounds is not None,
self.grenades is not None,
self.ticks is not None,
]
)
weapon_fires = apply_round_num(rounds, parse_weapon_fires(events))
grenades = apply_round_num(rounds, parse_grenades(parser))
ticks = apply_round_num(rounds, parse_ticks(parser))

return {
# Parser & Metadata
"file": file,
"parser": parser,
"header": header,
"events": events,
# Parsed from event dictionary
"kills": kills,
"damages": damages,
"bomb": bomb,
"smokes": smokes,
"infernos": infernos,
"weapon_fires": weapon_fires,
# Parsed from parser
"rounds": rounds,
"grenades": grenades,
"ticks": ticks,
}


def parse_header(parsed_header: dict) -> DemoHeader:
Expand Down
7 changes: 7 additions & 0 deletions awpy/stats/adr.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ def adr(
Returns:
pd.DataFrame: A dataframe of the player info + adr.
Raises:
ValueError: If damages are missing in the parsed demo.
"""
if not demo.damages:
missing_damages_error_msg = "Damages is missing in the parsed demo!"
raise ValueError(missing_damages_error_msg)

damages = demo.damages

# Remove team damage
Expand Down
11 changes: 11 additions & 0 deletions awpy/stats/kast.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,18 @@ def kast(demo: Demo, trade_ticks: int = 128 * 5) -> pd.DataFrame:
Returns:
pd.DataFrame: A dataframe of the player info + kast.
Raises:
ValueError: If kills or ticks are missing in the parsed demo.
"""
if not demo.kills:
missing_kills_error_msg = "Kills is missing in the parsed demo!"
raise ValueError(missing_kills_error_msg)

if not demo.ticks:
missing_ticks_error_msg = "Ticks is missing in the parsed demo!"
raise ValueError(missing_ticks_error_msg)

kills_with_trades = calculate_trades(demo.kills, trade_ticks)

# Get rounds where a player had a kill
Expand Down
22 changes: 22 additions & 0 deletions awpy/stats/rating.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ def impact(demo: Demo) -> pd.DataFrame:
Returns:
pd.DataFrame: A dataframe of the player info + impact.
Raises:
ValueError: If kills or ticks are missing in the parsed demo.
"""
if not demo.kills:
missing_kills_error_msg = "Kills is missing in the parsed demo!"
raise ValueError(missing_kills_error_msg)

if not demo.ticks:
missing_ticks_error_msg = "Ticks is missing in the parsed demo!"
raise ValueError(missing_ticks_error_msg)

# Get total rounds by player
player_total_rounds = get_player_rounds(demo)

Expand Down Expand Up @@ -103,7 +114,18 @@ def rating(demo: Demo) -> pd.DataFrame:
Returns:
pd.DataFrame: A dataframe of the player info + impact + rating.
Raises:
ValueError: If kills or ticks are missing in the parsed demo.
"""
if not demo.kills:
missing_kills_error_msg = "Kills is missing in the parsed demo!"
raise ValueError(missing_kills_error_msg)

if not demo.ticks:
missing_ticks_error_msg = "Ticks is missing in the parsed demo!"
raise ValueError(missing_ticks_error_msg)

# Get total rounds by player
player_total_rounds = get_player_rounds(demo)

Expand Down
7 changes: 7 additions & 0 deletions awpy/stats/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ def get_player_rounds(demo: Demo) -> pd.DataFrame:
Returns:
pd.DataFrame: A dataframe containing name, steamid, side, and n_rounds.
Raises:
ValueError: If ticks are missing in the parsed demo.
"""
if not demo.ticks:
missing_ticks_error_msg = "Ticks is missing in the parsed demo!"
raise ValueError(missing_ticks_error_msg)

# Get rounds played by each player/side
player_sides_by_round = demo.ticks.groupby(
["name", "steamid", "side", "round"]
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "awpy"
version = "2.0.0rc4"
version = "2.0.0rc1"
authors = [
{ name = "Peter Xenopoulos", email = "xenopoulos@nyu.edu" },
{ name = "Jan-Eric Nitschke", email = "janericnitschke@gmail.com" },
Expand Down Expand Up @@ -93,7 +93,7 @@ select = [
"RUF",
"EM",
]
ignore = ["D208", "ANN101", "T20", "PTH", "TRY003", "BLE001", "PLR2004"]
ignore = ["D208", "ANN101", "T20", "PTH", "TRY003", "BLE001", "PLR2004", "UP007"]

# Exclude a variety of commonly ignored directories.
exclude = [
Expand Down

0 comments on commit 1e14d66

Please sign in to comment.