copsapi-wrapper 1.0.0__tar.gz → 1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/CHANGELOG.md +11 -0
  2. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/PKG-INFO +75 -18
  3. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/README.md +74 -17
  4. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/pyproject.toml +1 -1
  5. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/__init__.py +38 -0
  6. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/_async_client.py +632 -630
  7. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/_core.py +225 -216
  8. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/_sync_client.py +5 -3
  9. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/cli.py +242 -242
  10. copsapi_wrapper-1.1.1/src/copsapi/enums.py +179 -0
  11. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/helpers.py +175 -175
  12. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/models.py +365 -365
  13. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/.gitignore +0 -0
  14. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/LICENSE +0 -0
  15. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/__main__.py +0 -0
  16. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/_io_async.py +0 -0
  17. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/_io_sync.py +0 -0
  18. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/cache.py +0 -0
  19. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/catalog.py +0 -0
  20. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/errors.py +0 -0
  21. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/py.typed +0 -0
  22. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/quota.py +0 -0
  23. {copsapi_wrapper-1.0.0 → copsapi_wrapper-1.1.1}/src/copsapi/watcher.py +0 -0
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.1
4
+
5
+ - On Python 3.10, constants such as `GameType.CASUAL` passed to client methods are now sent to the API as their plain values (`2`), not as their names.
6
+ - The README and examples use the named constants throughout.
7
+
8
+ ## 1.1.0
9
+
10
+ - Named constants for every code the API uses, such as `BanReason.HACKING`, `UserType.MODERATOR`, `MapId.CASTELLO`, `GameModeId.TOURNAMENT_MODE`, `LeaderboardType.KILLS` and `AutohostCommand.KICK`. Number codes are `IntEnum`s and text codes are `str` enums, so both compare equal to the plain values in the models.
11
+ - `InputDeviceFlag` is an `IntFlag`: `InputDeviceFlag(player.input_device_flags)` decodes the bitmask, and `has_input_device()` checks for one device.
12
+ - Every client method that takes a map, mode, game type, leaderboard type or command also accepts these constants. Nothing else changed, so existing code keeps working.
13
+
3
14
  ## 1.0.0
4
15
 
5
16
  First release, covering the Critical Ops Public API v2.0.0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: copsapi-wrapper
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: Typed, quota-aware Python client for the Critical Ops Public API v2, sync and async: profiles, leaderboards, match history, weapon stats, server status and Autohost with a live room watcher.
5
5
  Project-URL: Homepage, https://github.com/Matchmaking-Live/copsapi-wrapper-python
6
6
  Project-URL: Repository, https://github.com/Matchmaking-Live/copsapi-wrapper-python
@@ -80,6 +80,7 @@ It needs Python 3.10 or newer, and its only dependency is [httpx](https://www.py
80
80
  - [Errors](#errors)
81
81
  - [Caching](#caching)
82
82
  - [Game catalog](#game-catalog)
83
+ - [Named constants](#named-constants)
83
84
  - [Helpers](#helpers)
84
85
  - [Command line](#command-line)
85
86
  - [Models](#models)
@@ -94,7 +95,7 @@ What a token can do depends on its permissions:
94
95
  | Endpoints | Needs |
95
96
  | --- | --- |
96
97
  | Profiles, servers, match history, weapon stats, leaderboards | Any valid token |
97
- | Autohost (`create`, `poll`, `control`, `host`, the watcher) | A token with the `Autohost` permission |
98
+ | Autohost (`create`, `poll`, `control`, `host`, the watcher) | A token with the `Autohost` permission (Developer Issued) |
98
99
 
99
100
  There are three ways to pass the token:
100
101
 
@@ -128,14 +129,14 @@ If your token lacks `Autohost`, hosting calls raise `PermissionDeniedError` with
128
129
  ## Sync and async
129
130
 
130
131
  ```python
131
- from copsapi import CopsClient, AsyncCopsClient
132
+ from copsapi import CopsClient, AsyncCopsClient, LeaderboardType
132
133
 
133
134
  with CopsClient() as cops:
134
135
  servers = cops.servers.online()
135
136
 
136
137
  async with AsyncCopsClient() as cops:
137
138
  servers = await cops.servers.online()
138
- async for entry in cops.leaderboards.iterate("kills", max_pages=3):
139
+ async for entry in cops.leaderboards.iterate(LeaderboardType.KILLS, max_pages=3):
139
140
  print(entry.position, entry.basic_info.name)
140
141
  ```
141
142
 
@@ -211,9 +212,11 @@ server = cops.servers.pick(region="EU")
211
212
  ## Match history
212
213
 
213
214
  ```python
215
+ from copsapi import GameType
216
+
214
217
  history = cops.matches.history(user_id)
215
218
  match = cops.matches.one(history[0].id)
216
- recent = cops.matches.recent(user_id, limit=5, game_type="ranked")
219
+ recent = cops.matches.recent(user_id, limit=5, game_type=GameType.RANKED)
217
220
  ```
218
221
 
219
222
  - `history` returns the player's recent matches, newest first. The history resets with each game update.
@@ -223,26 +226,31 @@ recent = cops.matches.recent(user_id, limit=5, game_type="ranked")
223
226
  ## Weapon stats
224
227
 
225
228
  ```python
226
- weapons = cops.stats.weapons(user_id, "ranked")
227
- best = cops.stats.top_weapons(user_id, by="k", limit=5)
229
+ from copsapi import GameType
230
+
231
+ ranked = cops.stats.weapons(user_id, GameType.RANKED)
232
+ casual = cops.stats.weapons(user_id, GameType.CASUAL)
233
+ best = cops.stats.top_weapons(user_id, game_type=GameType.RANKED, by="k", limit=5)
228
234
  ```
229
235
 
230
- - The game type is `"ranked"` or `"casual"`. Custom games count as casual.
236
+ - The game type is `GameType.RANKED` or `GameType.CASUAL`; the strings `"ranked"` and `"casual"` work too. Custom games count as casual.
231
237
  - `top_weapons` sorts by kills (`k`), assists (`a`) or headshots (`h`). Each `WeaponStats` also has a `.headshot_rate`.
232
238
 
233
239
  ## Leaderboards
234
240
 
235
241
  ```python
236
- page = cops.leaderboards.page("mmr", page=1)
237
- elite = cops.leaderboards.page("mmr", rank=9)
242
+ from copsapi import LeaderboardType
238
243
 
239
- for entry in cops.leaderboards.iterate("kills", max_pages=10):
244
+ page = cops.leaderboards.page(LeaderboardType.MMR, page=1)
245
+ elite = cops.leaderboards.page(LeaderboardType.MMR, rank=9)
246
+
247
+ for entry in cops.leaderboards.iterate(LeaderboardType.KILLS, max_pages=10):
240
248
  print(entry.position, entry.basic_info.name, entry.score)
241
249
 
242
- top50 = cops.leaderboards.top("wins", 50)
250
+ top50 = cops.leaderboards.top(LeaderboardType.WINS, 50)
243
251
  ```
244
252
 
245
- - The types are `mmr`, `kills`, `deaths`, `assists`, `matches`, `wins`, `losses`, `xp` and `mvp`. Pages hold 10 entries.
253
+ - The types are `LeaderboardType.MMR`, `KILLS`, `DEATHS`, `ASSISTS`, `MATCHES`, `WINS`, `LOSSES`, `XP` and `MVP`; the plain strings (`"mmr"`, `"kills"` and so on) work too. Pages hold 10 entries.
246
254
  - `page` returns a `LeaderboardPage(type, page, rank, size, pages, entries)`. `rank` (0 to 9) filters to one rank.
247
255
  - Players move as matches end, so `iterate` skips anyone already yielded on an earlier page.
248
256
 
@@ -253,25 +261,27 @@ This needs a token with the `Autohost` permission. To check before offering host
253
261
  ### One call: host, wait for the code, add owners
254
262
 
255
263
  ```python
264
+ from copsapi import GameModeId, MapId
265
+
256
266
  server = cops.servers.pick(region="EU")
257
267
  room = cops.autohost.host(
258
268
  server_id=server.id,
259
- mode="tournament",
260
- map="Castello",
269
+ mode=GameModeId.TOURNAMENT_MODE,
270
+ map=MapId.CASTELLO,
261
271
  room_name="Scrim #42",
262
272
  owners=[captain_user_id],
263
273
  )
264
274
  print(f"Join with code {room.code}, password {room.password}")
265
275
  ```
266
276
 
267
- - `mode` and `map` accept names (`"Tournament Mode"`, `"castello"`) or ids. Combinations the API doesn't allow raise `BadRequestError` before the request is sent; pass `validate=False` if Critical Force added a map after this release.
277
+ - `mode` and `map` take a `GameModeId` and a `MapId`. Names (`"Tournament Mode"`, `"castello"`) and raw ids work too. Combinations the API doesn't allow raise `BadRequestError` before the request is sent; pass `validate=False` if Critical Force added a map after this release.
268
278
  - A random 6-character password is generated unless you pass `password="..."`. Pass `password=False` for an open room.
269
279
  - `host` raises `GameRejectedError` if the game server refuses the game (it's full or in maintenance, or the token already hosts 10 games there). It raises `GameGoneError`, or `ConnectionFailedError` with code `timeout`, if the room never appears.
270
280
 
271
281
  ### Step by step
272
282
 
273
283
  ```python
274
- game = cops.autohost.create(server_id=6, mode=15847, map=15391, room_name="Final", password="abc")
284
+ game = cops.autohost.create(server_id=6, mode=GameModeId.DEFUSE, map=MapId.RAID, room_name="Final", password="abc")
275
285
  ready = cops.autohost.wait_for_room(game.id)
276
286
  games = cops.autohost.poll([game.id, other_id])
277
287
  one = cops.autohost.get(game.id)
@@ -284,12 +294,14 @@ one = cops.autohost.get(game.id)
284
294
  ### Room commands
285
295
 
286
296
  ```python
297
+ from copsapi import AutohostCommand
298
+
287
299
  cops.autohost.kick(game_id, user_id)
288
300
  cops.autohost.add_owner(game_id, user_id)
289
301
  cops.autohost.remove_owner(game_id, user_id)
290
302
  cops.autohost.restart(game_id)
291
303
  cops.autohost.close(game_id)
292
- cops.autohost.control(game_id, "kick", user_id)
304
+ cops.autohost.control(game_id, AutohostCommand.KICK, user_id)
293
305
  ```
294
306
 
295
307
  - `kick` removes the player and keeps them out for an hour.
@@ -507,6 +519,51 @@ The package also exports these label tables:
507
519
  - `BAN_REASONS`, `USER_TYPES`, `ACCOUNT_AGES`, `PARTY_INVITE_PERMISSIONS` and `CLAN_RANKS`, which map API codes to readable text.
508
520
  - `LEADERBOARD_TYPES`, `MATCH_STATES`, `GAME_TYPES`, `INPUT_DEVICES` and `AUTOHOST_COMMANDS`.
509
521
 
522
+ ## Named constants
523
+
524
+ Every code the API uses has a named constant, so you don't have to remember what `2` or `15847` means:
525
+
526
+ ```python
527
+ from copsapi import BanReason, UserType, MapId, GameModeId, LeaderboardType, AutohostCommand, InputDeviceFlag
528
+
529
+ if profile.ban and profile.ban.reason == BanReason.HACKING:
530
+ flag_for_review(profile)
531
+ if player.user_type == UserType.MODERATOR:
532
+ print("a moderator is in the room")
533
+
534
+ cops.autohost.create(server_id=server_id, map=MapId.CASTELLO, mode=GameModeId.TOURNAMENT_MODE, room_name="Final")
535
+ cops.autohost.control(game_id, AutohostCommand.KICK, user_id)
536
+ page = cops.leaderboards.page(LeaderboardType.KILLS)
537
+
538
+ devices = InputDeviceFlag(player.input_device_flags)
539
+ if InputDeviceFlag.TOUCH in devices:
540
+ print("plays on a touch screen")
541
+ ```
542
+
543
+ | Constant | Kind | Members |
544
+ | --- | --- | --- |
545
+ | `UserType` | `IntEnum` | `SPECTATOR`, `USER`, `ASSISTANT_MOD`, `MODERATOR`, `VETERAN_MOD`, `DEVELOPER` |
546
+ | `BanType` | `IntEnum` | `PERMANENT`, `TEMPORARY` |
547
+ | `BanReason` | `IntEnum` | `UNKNOWN`, `FRAUD_ATTEMPT`, `HACKING`, `GRIEFING`, `VERBAL_ABUSE`, `IDLE`, `SPAM`, `REPORT_ABUSE`, `ACCOUNT_TRADING`, `GLITCHING`, `GHOSTING`, `PARTYING_WITH_HACKER`, `UNDER_INVESTIGATION`, `MARKETPLACE_ABUSE` |
548
+ | `ClanMemberRank` | `IntEnum` | `MEMBER`, `OFFICER`, `CO_LEADER`, `LEADER` |
549
+ | `PartyInvitePermission` | `IntEnum` | `ANYBODY`, `ONLY_FRIENDS_AND_CLAN_MEMBERS`, `NOBODY` |
550
+ | `GameType` | `IntEnum` | `RANKED`, `CASUAL` |
551
+ | `MatchResult` | `IntEnum` | `WIN`, `LOSS`, `DRAW` |
552
+ | `InputDeviceFlag` | `IntFlag` | `KEYBOARD_MOUSE`, `TOUCH`, `GAMEPAD` |
553
+ | `MapId` | `IntEnum` | Every map, such as `CANALS` or `CASTELLO` |
554
+ | `GameModeId` | `IntEnum` | Every hostable mode, such as `DEFUSE`, `TEAM_DEATHMATCH` or `TOURNAMENT_MODE` |
555
+ | `AccountAge` | `str` enum | `UNDER_A_WEEK` (`"1w"`) to `FOUR_YEARS_OR_MORE` (`"5y+"`) |
556
+ | `WinCondition` | `str` enum | `TEAM_ELIMINATED`, `TEAM_KILLS`, `TIME_UP`, `SURRENDERED` |
557
+ | `LeaveReason` | `str` enum | `LEFT`, `TIMEOUT`, `KICKED`, `BANNED`, `PING_LIMIT`, `AFK` |
558
+ | `MatchState` | `str` enum | `WAITING_FOR_PLAYERS`, `STARTING`, `FREEZETIME`, `ON_GOING`, `BOMB_PLANTED`, `WIN`, `TEAM_FLIP`, `GAME_END`, `CHANGING_MAP` |
559
+ | `AutohostCommand` | `str` enum | `KICK`, `RESTART`, `ROOM_OWNER_ADD`, `ROOM_OWNER_REMOVE`, `CLOSE` |
560
+ | `LeaderboardType` | `str` enum | `MMR`, `KILLS`, `DEATHS`, `ASSISTS`, `MATCHES`, `WINS`, `LOSSES`, `XP`, `MVP` |
561
+ | `GameStatus` | `str` enum | `OPEN`, `ENDED` |
562
+
563
+ The models keep plain `int` and `str` values, and every constant compares equal to them: `BanReason.HACKING == 2` and `LeaveReason.AFK == "AFK"` are both true. Every client method that takes a map, mode, game type, leaderboard type or command also accepts the constants.
564
+
565
+ Some values can grow as Critical Force updates the game: maps, game modes, win conditions, leave reasons and match states. The fields stay plain values, so a new map or reason never raises an error while parsing a response. If you convert a value yourself, for example `MapId(game.room.map_id)`, handle the `ValueError` you get for a value added after this release.
566
+
510
567
  ## Helpers
511
568
 
512
569
  | Helper | Returns |
@@ -45,6 +45,7 @@ It needs Python 3.10 or newer, and its only dependency is [httpx](https://www.py
45
45
  - [Errors](#errors)
46
46
  - [Caching](#caching)
47
47
  - [Game catalog](#game-catalog)
48
+ - [Named constants](#named-constants)
48
49
  - [Helpers](#helpers)
49
50
  - [Command line](#command-line)
50
51
  - [Models](#models)
@@ -59,7 +60,7 @@ What a token can do depends on its permissions:
59
60
  | Endpoints | Needs |
60
61
  | --- | --- |
61
62
  | Profiles, servers, match history, weapon stats, leaderboards | Any valid token |
62
- | Autohost (`create`, `poll`, `control`, `host`, the watcher) | A token with the `Autohost` permission |
63
+ | Autohost (`create`, `poll`, `control`, `host`, the watcher) | A token with the `Autohost` permission (Developer Issued) |
63
64
 
64
65
  There are three ways to pass the token:
65
66
 
@@ -93,14 +94,14 @@ If your token lacks `Autohost`, hosting calls raise `PermissionDeniedError` with
93
94
  ## Sync and async
94
95
 
95
96
  ```python
96
- from copsapi import CopsClient, AsyncCopsClient
97
+ from copsapi import CopsClient, AsyncCopsClient, LeaderboardType
97
98
 
98
99
  with CopsClient() as cops:
99
100
  servers = cops.servers.online()
100
101
 
101
102
  async with AsyncCopsClient() as cops:
102
103
  servers = await cops.servers.online()
103
- async for entry in cops.leaderboards.iterate("kills", max_pages=3):
104
+ async for entry in cops.leaderboards.iterate(LeaderboardType.KILLS, max_pages=3):
104
105
  print(entry.position, entry.basic_info.name)
105
106
  ```
106
107
 
@@ -176,9 +177,11 @@ server = cops.servers.pick(region="EU")
176
177
  ## Match history
177
178
 
178
179
  ```python
180
+ from copsapi import GameType
181
+
179
182
  history = cops.matches.history(user_id)
180
183
  match = cops.matches.one(history[0].id)
181
- recent = cops.matches.recent(user_id, limit=5, game_type="ranked")
184
+ recent = cops.matches.recent(user_id, limit=5, game_type=GameType.RANKED)
182
185
  ```
183
186
 
184
187
  - `history` returns the player's recent matches, newest first. The history resets with each game update.
@@ -188,26 +191,31 @@ recent = cops.matches.recent(user_id, limit=5, game_type="ranked")
188
191
  ## Weapon stats
189
192
 
190
193
  ```python
191
- weapons = cops.stats.weapons(user_id, "ranked")
192
- best = cops.stats.top_weapons(user_id, by="k", limit=5)
194
+ from copsapi import GameType
195
+
196
+ ranked = cops.stats.weapons(user_id, GameType.RANKED)
197
+ casual = cops.stats.weapons(user_id, GameType.CASUAL)
198
+ best = cops.stats.top_weapons(user_id, game_type=GameType.RANKED, by="k", limit=5)
193
199
  ```
194
200
 
195
- - The game type is `"ranked"` or `"casual"`. Custom games count as casual.
201
+ - The game type is `GameType.RANKED` or `GameType.CASUAL`; the strings `"ranked"` and `"casual"` work too. Custom games count as casual.
196
202
  - `top_weapons` sorts by kills (`k`), assists (`a`) or headshots (`h`). Each `WeaponStats` also has a `.headshot_rate`.
197
203
 
198
204
  ## Leaderboards
199
205
 
200
206
  ```python
201
- page = cops.leaderboards.page("mmr", page=1)
202
- elite = cops.leaderboards.page("mmr", rank=9)
207
+ from copsapi import LeaderboardType
203
208
 
204
- for entry in cops.leaderboards.iterate("kills", max_pages=10):
209
+ page = cops.leaderboards.page(LeaderboardType.MMR, page=1)
210
+ elite = cops.leaderboards.page(LeaderboardType.MMR, rank=9)
211
+
212
+ for entry in cops.leaderboards.iterate(LeaderboardType.KILLS, max_pages=10):
205
213
  print(entry.position, entry.basic_info.name, entry.score)
206
214
 
207
- top50 = cops.leaderboards.top("wins", 50)
215
+ top50 = cops.leaderboards.top(LeaderboardType.WINS, 50)
208
216
  ```
209
217
 
210
- - The types are `mmr`, `kills`, `deaths`, `assists`, `matches`, `wins`, `losses`, `xp` and `mvp`. Pages hold 10 entries.
218
+ - The types are `LeaderboardType.MMR`, `KILLS`, `DEATHS`, `ASSISTS`, `MATCHES`, `WINS`, `LOSSES`, `XP` and `MVP`; the plain strings (`"mmr"`, `"kills"` and so on) work too. Pages hold 10 entries.
211
219
  - `page` returns a `LeaderboardPage(type, page, rank, size, pages, entries)`. `rank` (0 to 9) filters to one rank.
212
220
  - Players move as matches end, so `iterate` skips anyone already yielded on an earlier page.
213
221
 
@@ -218,25 +226,27 @@ This needs a token with the `Autohost` permission. To check before offering host
218
226
  ### One call: host, wait for the code, add owners
219
227
 
220
228
  ```python
229
+ from copsapi import GameModeId, MapId
230
+
221
231
  server = cops.servers.pick(region="EU")
222
232
  room = cops.autohost.host(
223
233
  server_id=server.id,
224
- mode="tournament",
225
- map="Castello",
234
+ mode=GameModeId.TOURNAMENT_MODE,
235
+ map=MapId.CASTELLO,
226
236
  room_name="Scrim #42",
227
237
  owners=[captain_user_id],
228
238
  )
229
239
  print(f"Join with code {room.code}, password {room.password}")
230
240
  ```
231
241
 
232
- - `mode` and `map` accept names (`"Tournament Mode"`, `"castello"`) or ids. Combinations the API doesn't allow raise `BadRequestError` before the request is sent; pass `validate=False` if Critical Force added a map after this release.
242
+ - `mode` and `map` take a `GameModeId` and a `MapId`. Names (`"Tournament Mode"`, `"castello"`) and raw ids work too. Combinations the API doesn't allow raise `BadRequestError` before the request is sent; pass `validate=False` if Critical Force added a map after this release.
233
243
  - A random 6-character password is generated unless you pass `password="..."`. Pass `password=False` for an open room.
234
244
  - `host` raises `GameRejectedError` if the game server refuses the game (it's full or in maintenance, or the token already hosts 10 games there). It raises `GameGoneError`, or `ConnectionFailedError` with code `timeout`, if the room never appears.
235
245
 
236
246
  ### Step by step
237
247
 
238
248
  ```python
239
- game = cops.autohost.create(server_id=6, mode=15847, map=15391, room_name="Final", password="abc")
249
+ game = cops.autohost.create(server_id=6, mode=GameModeId.DEFUSE, map=MapId.RAID, room_name="Final", password="abc")
240
250
  ready = cops.autohost.wait_for_room(game.id)
241
251
  games = cops.autohost.poll([game.id, other_id])
242
252
  one = cops.autohost.get(game.id)
@@ -249,12 +259,14 @@ one = cops.autohost.get(game.id)
249
259
  ### Room commands
250
260
 
251
261
  ```python
262
+ from copsapi import AutohostCommand
263
+
252
264
  cops.autohost.kick(game_id, user_id)
253
265
  cops.autohost.add_owner(game_id, user_id)
254
266
  cops.autohost.remove_owner(game_id, user_id)
255
267
  cops.autohost.restart(game_id)
256
268
  cops.autohost.close(game_id)
257
- cops.autohost.control(game_id, "kick", user_id)
269
+ cops.autohost.control(game_id, AutohostCommand.KICK, user_id)
258
270
  ```
259
271
 
260
272
  - `kick` removes the player and keeps them out for an hour.
@@ -472,6 +484,51 @@ The package also exports these label tables:
472
484
  - `BAN_REASONS`, `USER_TYPES`, `ACCOUNT_AGES`, `PARTY_INVITE_PERMISSIONS` and `CLAN_RANKS`, which map API codes to readable text.
473
485
  - `LEADERBOARD_TYPES`, `MATCH_STATES`, `GAME_TYPES`, `INPUT_DEVICES` and `AUTOHOST_COMMANDS`.
474
486
 
487
+ ## Named constants
488
+
489
+ Every code the API uses has a named constant, so you don't have to remember what `2` or `15847` means:
490
+
491
+ ```python
492
+ from copsapi import BanReason, UserType, MapId, GameModeId, LeaderboardType, AutohostCommand, InputDeviceFlag
493
+
494
+ if profile.ban and profile.ban.reason == BanReason.HACKING:
495
+ flag_for_review(profile)
496
+ if player.user_type == UserType.MODERATOR:
497
+ print("a moderator is in the room")
498
+
499
+ cops.autohost.create(server_id=server_id, map=MapId.CASTELLO, mode=GameModeId.TOURNAMENT_MODE, room_name="Final")
500
+ cops.autohost.control(game_id, AutohostCommand.KICK, user_id)
501
+ page = cops.leaderboards.page(LeaderboardType.KILLS)
502
+
503
+ devices = InputDeviceFlag(player.input_device_flags)
504
+ if InputDeviceFlag.TOUCH in devices:
505
+ print("plays on a touch screen")
506
+ ```
507
+
508
+ | Constant | Kind | Members |
509
+ | --- | --- | --- |
510
+ | `UserType` | `IntEnum` | `SPECTATOR`, `USER`, `ASSISTANT_MOD`, `MODERATOR`, `VETERAN_MOD`, `DEVELOPER` |
511
+ | `BanType` | `IntEnum` | `PERMANENT`, `TEMPORARY` |
512
+ | `BanReason` | `IntEnum` | `UNKNOWN`, `FRAUD_ATTEMPT`, `HACKING`, `GRIEFING`, `VERBAL_ABUSE`, `IDLE`, `SPAM`, `REPORT_ABUSE`, `ACCOUNT_TRADING`, `GLITCHING`, `GHOSTING`, `PARTYING_WITH_HACKER`, `UNDER_INVESTIGATION`, `MARKETPLACE_ABUSE` |
513
+ | `ClanMemberRank` | `IntEnum` | `MEMBER`, `OFFICER`, `CO_LEADER`, `LEADER` |
514
+ | `PartyInvitePermission` | `IntEnum` | `ANYBODY`, `ONLY_FRIENDS_AND_CLAN_MEMBERS`, `NOBODY` |
515
+ | `GameType` | `IntEnum` | `RANKED`, `CASUAL` |
516
+ | `MatchResult` | `IntEnum` | `WIN`, `LOSS`, `DRAW` |
517
+ | `InputDeviceFlag` | `IntFlag` | `KEYBOARD_MOUSE`, `TOUCH`, `GAMEPAD` |
518
+ | `MapId` | `IntEnum` | Every map, such as `CANALS` or `CASTELLO` |
519
+ | `GameModeId` | `IntEnum` | Every hostable mode, such as `DEFUSE`, `TEAM_DEATHMATCH` or `TOURNAMENT_MODE` |
520
+ | `AccountAge` | `str` enum | `UNDER_A_WEEK` (`"1w"`) to `FOUR_YEARS_OR_MORE` (`"5y+"`) |
521
+ | `WinCondition` | `str` enum | `TEAM_ELIMINATED`, `TEAM_KILLS`, `TIME_UP`, `SURRENDERED` |
522
+ | `LeaveReason` | `str` enum | `LEFT`, `TIMEOUT`, `KICKED`, `BANNED`, `PING_LIMIT`, `AFK` |
523
+ | `MatchState` | `str` enum | `WAITING_FOR_PLAYERS`, `STARTING`, `FREEZETIME`, `ON_GOING`, `BOMB_PLANTED`, `WIN`, `TEAM_FLIP`, `GAME_END`, `CHANGING_MAP` |
524
+ | `AutohostCommand` | `str` enum | `KICK`, `RESTART`, `ROOM_OWNER_ADD`, `ROOM_OWNER_REMOVE`, `CLOSE` |
525
+ | `LeaderboardType` | `str` enum | `MMR`, `KILLS`, `DEATHS`, `ASSISTS`, `MATCHES`, `WINS`, `LOSSES`, `XP`, `MVP` |
526
+ | `GameStatus` | `str` enum | `OPEN`, `ENDED` |
527
+
528
+ The models keep plain `int` and `str` values, and every constant compares equal to them: `BanReason.HACKING == 2` and `LeaveReason.AFK == "AFK"` are both true. Every client method that takes a map, mode, game type, leaderboard type or command also accepts the constants.
529
+
530
+ Some values can grow as Critical Force updates the game: maps, game modes, win conditions, leave reasons and match states. The fields stay plain values, so a new map or reason never raises an error while parsing a response. If you convert a value yourself, for example `MapId(game.room.map_id)`, handle the `ValueError` you get for a value added after this release.
531
+
475
532
  ## Helpers
476
533
 
477
534
  | Helper | Returns |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "copsapi-wrapper"
7
- version = "1.0.0"
7
+ version = "1.1.1"
8
8
  description = "Typed, quota-aware Python client for the Critical Ops Public API v2, sync and async: profiles, leaderboards, match history, weapon stats, server status and Autohost with a live room watcher."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -39,6 +39,26 @@ from .catalog import (
39
39
  maps_for,
40
40
  modes_for,
41
41
  )
42
+ from .enums import (
43
+ AccountAge,
44
+ AutohostCommand,
45
+ BanReason,
46
+ BanType,
47
+ ClanMemberRank,
48
+ GameModeId,
49
+ GameStatus,
50
+ GameType,
51
+ InputDeviceFlag,
52
+ LeaderboardType,
53
+ LeaveReason,
54
+ MapId,
55
+ MatchResult,
56
+ MatchState,
57
+ PartyInvitePermission,
58
+ UserType,
59
+ WinCondition,
60
+ has_input_device,
61
+ )
42
62
  from .errors import (
43
63
  AuthenticationError,
44
64
  BadRequestError,
@@ -103,6 +123,24 @@ from .watcher import EVENTS, AsyncAutohostWatcher, AutohostWatcher, JoinInfo
103
123
  __version__ = VERSION
104
124
 
105
125
  __all__ = [
126
+ "AccountAge",
127
+ "AutohostCommand",
128
+ "BanReason",
129
+ "BanType",
130
+ "ClanMemberRank",
131
+ "GameModeId",
132
+ "GameStatus",
133
+ "GameType",
134
+ "InputDeviceFlag",
135
+ "LeaderboardType",
136
+ "LeaveReason",
137
+ "MapId",
138
+ "MatchResult",
139
+ "MatchState",
140
+ "PartyInvitePermission",
141
+ "UserType",
142
+ "WinCondition",
143
+ "has_input_device",
106
144
  "ACCOUNT_AGES",
107
145
  "AUTOHOST_COMMANDS",
108
146
  "AsyncAutohostApi",