or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

tessl/pypi-python-a2s

Library to query Source and GoldSource game servers using Valve's Server Query Protocol.

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
pypipkg:pypi/python-a2s@1.4.x

To install, run

npx @tessl/cli install tessl/pypi-python-a2s@1.4.0

0

# Python A2S

1

2

Library to query Source and GoldSource game servers using Valve's Server Query Protocol. Enables developers to retrieve server information, player details, and server configuration from game servers including Half-Life, Team Fortress 2, Counter-Strike, and other Source engine games.

3

4

## Package Information

5

6

- **Package Name**: python-a2s

7

- **Language**: Python

8

- **Installation**: `pip install python-a2s`

9

- **Requirements**: Python >=3.9, no external dependencies

10

11

## Core Imports

12

13

```python

14

import a2s

15

```

16

17

Import specific functions:

18

19

```python

20

from a2s import info, players, rules, ainfo, aplayers, arules

21

from a2s import SourceInfo, GoldSrcInfo, Player

22

from a2s import BrokenMessageError, BufferExhaustedError

23

```

24

25

## Basic Usage

26

27

```python

28

import a2s

29

30

# Server address (IP and port)

31

address = ("chi-1.us.uncletopia.com", 27015)

32

33

# Query server information

34

server_info = a2s.info(address)

35

print(f"Server: {server_info.server_name}")

36

print(f"Map: {server_info.map_name}")

37

print(f"Players: {server_info.player_count}/{server_info.max_players}")

38

39

# Query current players

40

player_list = a2s.players(address)

41

for player in player_list:

42

print(f"Player: {player.name}, Score: {player.score}")

43

44

# Query server rules/configuration

45

server_rules = a2s.rules(address)

46

print(f"Game mode: {server_rules.get('deathmatch', 'unknown')}")

47

```

48

49

## Architecture

50

51

The library implements Valve's Server Query Protocol with a clean separation between protocol handling and transport mechanisms:

52

53

- **Protocol Layer**: Dedicated protocol classes (`InfoProtocol`, `PlayersProtocol`, `RulesProtocol`) handle message serialization/deserialization for each query type

54

- **Transport Layer**: Separate sync (`a2s_sync`) and async (`a2s_async`) implementations handle UDP communication and response management

55

- **Data Models**: Strongly typed dataclasses (`SourceInfo`, `GoldSrcInfo`, `Player`) represent server responses with engine-specific variations

56

- **Error Handling**: Custom exception hierarchy for protocol-level errors with fallback to standard networking exceptions

57

58

This design enables both synchronous and asynchronous usage patterns while maintaining type safety and providing consistent error handling across different game server engines.

59

60

## Capabilities

61

62

### Server Information Queries

63

64

Query server details including name, map, player count, game type, and various server properties. Returns different data structures for Source and GoldSource engines.

65

66

```python { .api }

67

def info(

68

address: tuple[str, int],

69

timeout: float = 3.0,

70

encoding: str | None = "utf-8"

71

) -> SourceInfo[str] | SourceInfo[bytes] | GoldSrcInfo[str] | GoldSrcInfo[bytes]:

72

"""

73

Query server information.

74

75

Parameters:

76

- address: Server address as (IP, port) tuple

77

- timeout: Query timeout in seconds (default: 3.0)

78

- encoding: String encoding or None for raw bytes (default: "utf-8")

79

80

Returns:

81

SourceInfo or GoldSrcInfo object containing server details

82

"""

83

84

async def ainfo(

85

address: tuple[str, int],

86

timeout: float = 3.0,

87

encoding: str | None = "utf-8"

88

) -> SourceInfo[str] | SourceInfo[bytes] | GoldSrcInfo[str] | GoldSrcInfo[bytes]:

89

"""

90

Async version of info().

91

"""

92

```

93

94

### Player Queries

95

96

Retrieve list of players currently connected to the server with their names, scores, and connection durations.

97

98

```python { .api }

99

def players(

100

address: tuple[str, int],

101

timeout: float = 3.0,

102

encoding: str | None = "utf-8"

103

) -> list[Player[str]] | list[Player[bytes]]:

104

"""

105

Query list of players on server.

106

107

Parameters:

108

- address: Server address as (IP, port) tuple

109

- timeout: Query timeout in seconds (default: 3.0)

110

- encoding: String encoding or None for raw bytes (default: "utf-8")

111

112

Returns:

113

List of Player objects

114

"""

115

116

async def aplayers(

117

address: tuple[str, int],

118

timeout: float = 3.0,

119

encoding: str | None = "utf-8"

120

) -> list[Player[str]] | list[Player[bytes]]:

121

"""

122

Async version of players().

123

"""

124

```

125

126

### Server Rules Queries

127

128

Retrieve server configuration settings and rules as key-value pairs.

129

130

```python { .api }

131

def rules(

132

address: tuple[str, int],

133

timeout: float = 3.0,

134

encoding: str | None = "utf-8"

135

) -> dict[str, str] | dict[bytes, bytes]:

136

"""

137

Query server rules and configuration.

138

139

Parameters:

140

- address: Server address as (IP, port) tuple

141

- timeout: Query timeout in seconds (default: 3.0)

142

- encoding: String encoding or None for raw bytes (default: "utf-8")

143

144

Returns:

145

Dictionary of rule names to values

146

"""

147

148

async def arules(

149

address: tuple[str, int],

150

timeout: float = 3.0,

151

encoding: str | None = "utf-8"

152

) -> dict[str, str] | dict[bytes, bytes]:

153

"""

154

Async version of rules().

155

"""

156

```

157

158

## Types

159

160

### Server Information Types

161

162

#### Source Engine Servers

163

164

```python { .api }

165

@dataclass

166

class SourceInfo(Generic[StrType]):

167

"""Information from Source engine servers (Half-Life 2, TF2, CS:GO, etc.)"""

168

169

protocol: int

170

"""Protocol version used by the server"""

171

172

server_name: StrType

173

"""Display name of the server"""

174

175

map_name: StrType

176

"""The currently loaded map"""

177

178

folder: StrType

179

"""Name of the game directory"""

180

181

game: StrType

182

"""Name of the game"""

183

184

app_id: int

185

"""App ID of the game required to connect"""

186

187

player_count: int

188

"""Number of players currently connected"""

189

190

max_players: int

191

"""Number of player slots available"""

192

193

bot_count: int

194

"""Number of bots on the server"""

195

196

server_type: StrType

197

"""Type of server: 'd' (dedicated), 'l' (non-dedicated), 'p' (SourceTV proxy)"""

198

199

platform: StrType

200

"""Operating system: 'l' (Linux), 'w' (Windows), 'm' (macOS)"""

201

202

password_protected: bool

203

"""Server requires a password to connect"""

204

205

vac_enabled: bool

206

"""Server has VAC (Valve Anti-Cheat) enabled"""

207

208

version: StrType

209

"""Version of the server software"""

210

211

edf: int

212

"""Extra data field indicating which optional fields are present"""

213

214

ping: float

215

"""Round-trip time for the request in seconds"""

216

217

# Optional fields (presence indicated by edf flags):

218

port: int | None = None

219

"""Port of the game server"""

220

221

steam_id: int | None = None

222

"""Steam ID of the server"""

223

224

stv_port: int | None = None

225

"""Port of the SourceTV server"""

226

227

stv_name: StrType | None = None

228

"""Name of the SourceTV server"""

229

230

keywords: StrType | None = None

231

"""Tags that describe the gamemode being played"""

232

233

game_id: int | None = None

234

"""Game ID for games with app ID too high for 16-bit"""

235

236

# Properties to check optional field presence:

237

@property

238

def has_port(self) -> bool:

239

"""Check if port field is present"""

240

return bool(self.edf & 0x80)

241

242

@property

243

def has_steam_id(self) -> bool:

244

"""Check if steam_id field is present"""

245

return bool(self.edf & 0x10)

246

247

@property

248

def has_stv(self) -> bool:

249

"""Check if SourceTV fields are present"""

250

return bool(self.edf & 0x40)

251

252

@property

253

def has_keywords(self) -> bool:

254

"""Check if keywords field is present"""

255

return bool(self.edf & 0x20)

256

257

@property

258

def has_game_id(self) -> bool:

259

"""Check if game_id field is present"""

260

return bool(self.edf & 0x01)

261

```

262

263

#### GoldSource Engine Servers

264

265

```python { .api }

266

@dataclass

267

class GoldSrcInfo(Generic[StrType]):

268

"""Information from GoldSource engine servers (Half-Life 1, CS 1.6, etc.)"""

269

270

address: StrType

271

"""IP Address and port of the server"""

272

273

server_name: StrType

274

"""Display name of the server"""

275

276

map_name: StrType

277

"""The currently loaded map"""

278

279

folder: StrType

280

"""Name of the game directory"""

281

282

game: StrType

283

"""Name of the game"""

284

285

player_count: int

286

"""Number of players currently connected"""

287

288

max_players: int

289

"""Number of player slots available"""

290

291

protocol: int

292

"""Protocol version used by the server"""

293

294

server_type: StrType

295

"""Type of server: 'd' (dedicated), 'l' (non-dedicated), 'p' (SourceTV proxy)"""

296

297

platform: StrType

298

"""Operating system: 'l' (Linux), 'w' (Windows)"""

299

300

password_protected: bool

301

"""Server requires a password to connect"""

302

303

is_mod: bool

304

"""Server is running a Half-Life mod instead of the base game"""

305

306

vac_enabled: bool

307

"""Server has VAC enabled"""

308

309

bot_count: int

310

"""Number of bots on the server"""

311

312

ping: float

313

"""Round-trip time for the request in seconds"""

314

315

# Optional mod information (present if is_mod is True):

316

mod_website: StrType | None

317

"""URL to the mod website"""

318

319

mod_download: StrType | None

320

"""URL to download the mod"""

321

322

mod_version: int | None

323

"""Version of the mod installed on the server"""

324

325

mod_size: int | None

326

"""Size in bytes of the mod"""

327

328

multiplayer_only: bool | None

329

"""Mod supports multiplayer only"""

330

331

uses_custom_dll: bool | None

332

"""Mod uses a custom DLL"""

333

334

@property

335

def uses_hl_dll(self) -> bool | None:

336

"""Compatibility alias for uses_custom_dll"""

337

return self.uses_custom_dll

338

```

339

340

### Player Information Type

341

342

```python { .api }

343

@dataclass

344

class Player(Generic[StrType]):

345

"""Information about a player on the server"""

346

347

index: int

348

"""Entry index (usually 0)"""

349

350

name: StrType

351

"""Name of the player"""

352

353

score: int

354

"""Score of the player"""

355

356

duration: float

357

"""Time the player has been connected to the server in seconds"""

358

```

359

360

### Generic Type Parameters

361

362

```python { .api }

363

StrType = TypeVar("StrType", str, bytes)

364

"""Type variable for string vs bytes encoding"""

365

```

366

367

## Exception Types

368

369

```python { .api }

370

class BrokenMessageError(Exception):

371

"""General decoding error for malformed server responses"""

372

pass

373

374

class BufferExhaustedError(BrokenMessageError):

375

"""Raised when response data is shorter than expected"""

376

pass

377

```

378

379

## Configuration Constants

380

381

```python { .api }

382

DEFAULT_TIMEOUT: float = 3.0

383

"""Default timeout in seconds for server queries"""

384

385

DEFAULT_ENCODING: str = "utf-8"

386

"""Default string encoding for server responses"""

387

388

DEFAULT_RETRIES: int = 5

389

"""Default number of retry attempts for failed queries"""

390

```

391

392

## Error Handling

393

394

The library can raise several types of exceptions:

395

396

**Custom Exceptions:**

397

- `BrokenMessageError`: General decoding error for malformed responses

398

- `BufferExhaustedError`: Response data too short

399

400

**Standard Exceptions:**

401

- `socket.timeout`: No response (synchronous calls)

402

- `asyncio.TimeoutError`: No response (async calls)

403

- `socket.gaierror`: Address resolution error

404

- `ConnectionRefusedError`: Target port closed

405

- `OSError`: Various networking errors like routing failure

406

407

## Usage Examples

408

409

### Async Usage

410

411

```python

412

import asyncio

413

import a2s

414

415

async def query_server():

416

address = ("server.example.com", 27015)

417

418

# Use async versions

419

info = await a2s.ainfo(address)

420

players = await a2s.aplayers(address)

421

rules = await a2s.arules(address)

422

423

print(f"Server: {info.server_name}")

424

print(f"Players: {len(players)}")

425

print(f"Rules: {len(rules)} settings")

426

427

# Run async function

428

asyncio.run(query_server())

429

```

430

431

### Error Handling

432

433

```python

434

import a2s

435

import socket

436

437

address = ("server.example.com", 27015)

438

439

try:

440

info = a2s.info(address, timeout=5.0)

441

print(f"Server: {info.server_name}")

442

except a2s.BrokenMessageError:

443

print("Server sent malformed response")

444

except socket.timeout:

445

print("Server did not respond within timeout")

446

except ConnectionRefusedError:

447

print("Server port is closed")

448

except OSError as e:

449

print(f"Network error: {e}")

450

```

451

452

### Raw Bytes Mode

453

454

```python

455

import a2s

456

457

address = ("server.example.com", 27015)

458

459

# Get raw bytes instead of decoded strings

460

info = a2s.info(address, encoding=None)

461

print(f"Server name (bytes): {info.server_name}") # bytes object

462

463

# Player names as bytes

464

players = a2s.players(address, encoding=None)

465

for player in players:

466

print(f"Player (bytes): {player.name}") # bytes object

467

```