copsapi-wrapper (Python)
A typed, quota-aware Python client for the Critical Ops Public API v2, with both synchronous and asyncio clients.
pip install copsapi-wrapper
from copsapi import CopsClient
with CopsClient("cops_your_token_here") as cops:
player = cops.profiles.get("SomePlayer")
print(player.name, player.stats.ranked.mmr)
It covers every endpoint in the API: player profiles, leaderboards, match history, weapon stats, server status, and hosting custom games (Autohost). It also handles the parts you'd otherwise write yourself:
- Sync and async.
CopsClientandAsyncCopsClienthave the same methods and behave the same way. - Typed models. Responses come back as frozen dataclasses that match the official OpenAPI spec field for field. Fields the API adds later are kept in
.raw. The package shipspy.typedfor type checkers. - Quota management. The client reads the
X-RateLimit-*headers and waits for the next minute instead of hitting429. It can also keep a reserve of requests for hosting traffic. - Safe retries. Network errors, timeouts and the API's front proxy answering a plain-text
404 Endpoint Not Found.are retried.POSTrequests are never retried after a failure that could mean the API already acted, so a game is never hosted twice. - Batching. Profile lookups are split into requests of 20, match lookups into requests of 20, and game polling into requests of 50.
- Unknown players don't break a batch. The API fails a whole profile request when one player doesn't exist.
profiles.many()narrows down the missing players and returns everyone else. - Caching. Results are cached in memory, or in your own store such as Redis. The async client also combines identical requests made at the same time into one.
- An Autohost toolkit. Host a room and get its code in one call. A live watcher reports joins, leaves, disconnects, score changes, match results and the room ending.
- A game catalog. Every hostable game mode and map is included, with name lookup and validation before the request is sent.
- A CLI. The
copsapicommand does quick lookups, and hosts and watches rooms from a terminal.
It needs Python 3.10 or newer, and its only dependency is httpx. A JavaScript/TypeScript version is also available.
Contents
- Your token
- Sync and async
- Client options
- Profiles
- Servers
- Match history
- Weapon stats
- Leaderboards
- Hosting games (Autohost)
- Watching hosted games
- Rate limits and the quota
- Errors
- Caching
- Game catalog
- Named constants
- Helpers
- Command line
- Models
- Development
Your token
You need your own API token, issued by Critical Force to your Critical Ops account. This package doesn't include a token and never shares one. Tokens look like cops_ followed by 48 hex digits.
What a token can do depends on its permissions:
| Endpoints | Needs |
|---|---|
| Profiles, servers, match history, weapon stats, leaderboards | Any valid token |
Autohost (create, poll, control, host, the watcher) |
A token with the Autohost permission |
There are three ways to pass the token:
cops = CopsClient("cops_your_token_here")
cops = CopsClient(os.environ["MY_COPS_TOKEN"], timeout=5)
cops = CopsClient()
The last form reads COPS_API_TOKEN from the environment.
- The constructor raises
AuthenticationErrorwith codemissing_tokenif there is no token. - It raises
invalid_tokenif the token isn't in thecops_+ 48 hex format. Passvalidate_token=Falseif Critical Force changes the format.
A token acts on behalf of the account it was issued to, so keep it on a server and out of your repository.
Check your token and its permissions
check = cops.verify_token()
can_host = cops.autohost.is_available()
verify_tokenreturnsTokenCheck(valid, autohost, error, quota).is_availablereturnsTrueorFalse. The answer is remembered, so later calls are free.
You can also run copsapi token from the command line.
If your token lacks Autohost, hosting calls raise PermissionDeniedError with a message saying which permission is missing. After the first refusal, the client remembers the answer, and further hosting calls fail at once without spending quota. A running watcher stops by itself. Every other endpoint keeps working.
Sync and async
from copsapi import CopsClient, AsyncCopsClient
with CopsClient() as cops:
servers = cops.servers.online()
async with AsyncCopsClient() as cops:
servers = await cops.servers.online()
async for entry in cops.leaderboards.iterate("kills", max_pages=3):
print(entry.position, entry.basic_info.name)
Both clients have the same methods. Every method of the async client is awaited, and its iterate() is an async generator.
Use the client as a context manager, or call close() (await aclose() for the async client), so the HTTP connections are released.
Client options
| Option | Default | Description |
|---|---|---|
token |
COPS_API_TOKEN |
Your own API token. Required. It can also be the first positional argument. |
validate_token |
True |
Check the token format up front. |
base_url |
COPS_API_BASE, or https://latest.prod.copsapi.criticalforce.fi/api/public/v2 |
The API base URL. |
timeout |
10.0 |
Seconds allowed for each attempt. |
retries |
2 |
Retries for network errors, timeouts and 500/503 on GET requests. |
gateway_retries |
5 |
Retries when the front proxy answers a non-JSON 404/5xx page. |
max_rate_limit_wait |
15.0 |
On a 429, wait and retry if Retry-After is at most this long, otherwise raise. |
rate_limit |
60 |
Assumed requests per minute until the API reports the real number. |
rate_limit_reserve |
0 |
Requests per minute kept back for high-priority calls (all Autohost calls). |
on_quota_exhausted |
"wait" |
"wait" for the next minute, or "raise" a local RateLimitError. |
max_quota_wait |
65.0 |
Longest time to wait for quota before raising. |
max_concurrent |
8 |
Requests in flight at once (async client). |
dedupe |
True |
Combine identical GET requests made at the same time (async client). |
cache |
True |
True uses an in-memory cache, False turns caching off, and any store uses that store. |
cache_ttls |
see Caching | Override TTLs per kind of data, in seconds. |
max_players_per_request |
20 |
Players per /profile request. Raise it only if Critical Force raised your token's limit. |
auth_header |
"bearer" |
"x-api-key" sends the token as X-Api-Key instead. |
user_agent |
copsapi-wrapper-python/<version> |
None sends no User-Agent header. |
http_client |
none | Your own httpx.Client or httpx.AsyncClient, for proxies or custom TLS. |
transport |
none | An httpx transport, such as httpx.MockTransport in tests. |
on_request, on_response, on_retry |
none | Callbacks that receive a dict about each request, for logging and metrics. |
cops = CopsClient(
rate_limit_reserve=15,
on_retry=lambda info: log.warning("retrying %s (%s)", info["path"], info["reason"]),
on_response=lambda info: metrics.observe(info["path"], info["status"], info["duration"]),
)
Every read method accepts fresh=True to skip the cache.
Profiles
An int is a user id, and a str is a username.
one = cops.profiles.get(123456)
maybe = cops.profiles.find("NoSuchPlayer")
found = cops.profiles.many([123456, "Alice", "Bob"])
by_id = cops.profiles.by_ids([1, 2, 3])
by_name = cops.profiles.by_usernames(["Alice", "Bob"])
getraisesNotFoundErrorwhen the player doesn't exist.findreturnsNoneinstead of raising.manytakes any number of players. It returns them in request order without duplicates, and skips players who don't exist. Passskip_missing=Falseto raise instead.by_idsreturns adict[int, Profile].by_usernamesreturns adictkeyed by the names you asked for.
Servers
servers = cops.servers.list()
online = cops.servers.online()
server = cops.servers.pick(region="EU")
listreturns aGameServer(id, name, online, region)for every server.regionis guessed from the server name; seeregion_of().pickreturns a random online server in the region. If none match, it falls back to any online server; passfallback_to_any=Falseto getNoneinstead. You can also filter withname="DE".
Match history
history = cops.matches.history(user_id)
match = cops.matches.one(history[0].id)
recent = cops.matches.recent(user_id, limit=5, game_type="ranked")
historyreturns the player's recent matches, newest first. The history resets with each game update.onereturns a full scoreboard, andmatches.get([...])returns several (requests are split into groups of 20). Finished matches are cached for an hour.recentreturns(entry, match)pairs, combining history and scoreboards.
Weapon stats
weapons = cops.stats.weapons(user_id, "ranked")
best = cops.stats.top_weapons(user_id, by="k", limit=5)
- The game type is
"ranked"or"casual". Custom games count as casual. top_weaponssorts by kills (k), assists (a) or headshots (h). EachWeaponStatsalso has a.headshot_rate.
Leaderboards
page = cops.leaderboards.page("mmr", page=1)
elite = cops.leaderboards.page("mmr", rank=9)
for entry in cops.leaderboards.iterate("kills", max_pages=10):
print(entry.position, entry.basic_info.name, entry.score)
top50 = cops.leaderboards.top("wins", 50)
- The types are
mmr,kills,deaths,assists,matches,wins,losses,xpandmvp. Pages hold 10 entries. pagereturns aLeaderboardPage(type, page, rank, size, pages, entries).rank(0 to 9) filters to one rank.- Players move as matches end, so
iterateskips anyone already yielded on an earlier page.
Hosting games (Autohost)
This needs a token with the Autohost permission. To check before offering hosting features to your users, call cops.autohost.is_available().
One call: host, wait for the code, add owners
server = cops.servers.pick(region="EU")
room = cops.autohost.host(
server_id=server.id,
mode="tournament",
map="Castello",
room_name="Scrim #42",
owners=[captain_user_id],
)
print(f"Join with code {room.code}, password {room.password}")
modeandmapaccept names ("Tournament Mode","castello") or ids. Combinations the API doesn't allow raiseBadRequestErrorbefore the request is sent; passvalidate=Falseif Critical Force added a map after this release.- A random 6-character password is generated unless you pass
password="...". Passpassword=Falsefor an open room. hostraisesGameRejectedErrorif the game server refuses the game (it's full or in maintenance, or the token already hosts 10 games there). It raisesGameGoneError, orConnectionFailedErrorwith codetimeout, if the room never appears.
Step by step
game = cops.autohost.create(server_id=6, mode=15847, map=15391, room_name="Final", password="abc")
ready = cops.autohost.wait_for_room(game.id)
games = cops.autohost.poll([game.id, other_id])
one = cops.autohost.get(game.id)
wait_for_roomwaits until the room code appears.pollchecks many games at once (up to 50 per request, split automatically).getreturnsNoneonce the game is gone.
Room commands
cops.autohost.kick(game_id, user_id)
cops.autohost.add_owner(game_id, user_id)
cops.autohost.remove_owner(game_id, user_id)
cops.autohost.restart(game_id)
cops.autohost.close(game_id)
cops.autohost.control(game_id, "kick", user_id)
kickremoves the player and keeps them out for an hour.add_ownerworks before the player joins.closeends the game; the matches played so far stay in its results.controlis the general form; the other methods call it.
Rules from the API
- A room closes when nobody joins within 3 minutes, when everyone leaves, or after 3 hours.
- Each game server hosts at most 100 games, and at most 10 per token.
- Game ids are unique within the current game version only.
resultslists every match that was played to the end in the room. A match restarted before it ends leaves no result.
Watching hosted games
The watcher polls every game you track in a single request every 5 seconds, which is how often game servers update. It compares each poll with the previous one and calls your handlers.
watcher = cops.autohost.watch()
watcher.track(room.id, meta={"scrim_id": 42})
@watcher.on("player_join")
def joined(game, player, join):
print(player.user_id, "joined after", join.since_room_ready, "seconds")
@watcher.on("match_result")
def finished(game, result, index):
save_result(watcher.meta(game.id), summarize_result(result))
watcher.on("player_disconnect", lambda game, player: print(player.user_id, "lost connection"))
watcher.on("ended", lambda game: print("room closed", game.replay_id))
watcher.run()
run()blocks until every tracked game has ended.start()polls in a background thread instead. Stop it withstop(), or usewith watcher:.
With the async client, handlers can be plain functions or coroutines:
watcher = cops.autohost.watch()
watcher.track(room.id)
@watcher.on("score")
async def score(game, round_, scores):
await post_to_discord(f"Round {round_}: {scores[0]}-{scores[1]}")
await watcher.run()
await watcher.run()waits until every tracked game has ended.watcher.start()polls in a background task. Stop it withawait watcher.stop(), or useasync with watcher:.
Events
| Event | Handler arguments | When |
|---|---|---|
update |
game, previous |
Every poll. |
room_ready |
game |
The room code first appears. |
rejected |
game, reason |
The game server refused the game. |
state |
game, state, previous |
match_state changed. |
match_start |
game |
A match started or was restarted. |
score |
game, round, scores |
The round or scores changed. |
player_join |
game, player, JoinInfo(first_seen_at, since_room_ready) |
A player appeared in the room. |
player_leave |
game, player |
A player left the room. |
player_disconnect |
game, player |
Ping dropped to -1. |
player_reconnect |
game, player, offline_seconds |
Ping came back. |
player_team_change |
game, player, previous_team |
A player switched team. |
match_result |
game, result, index |
A match finished. Each result fires once. |
ended |
game |
The room closed. The game is untracked unless untrack_ended=False. |
gone |
game_id, last_known |
Polling stopped returning the game for missing_grace seconds. |
poll_error |
error |
A poll failed. The watcher keeps going, except for token and permission errors. |
handler_error |
error, event |
One of your handlers raised an exception. |
Options and methods
watch(interval=5.0, missing_grace=60.0, untrack_ended=True)track(game_id, meta=None, seen_results=0),untrack(game_id),poll_now()latest(game_id),meta(game_id),join_times(game_id),room_ready_at(game_id),ids,len(watcher),running
missing_grace exists because a game created a moment ago can be missing from the first polls.
Resuming after a restart
To resume after your process restarts, track the game ids again. Pass seen_results set to the number of results you already processed, so they aren't reported twice.
Late joiners
join_times(game_id) and the player_join event tell you how long each player took to join after the room was ready. You can use that to flag no-shows.
Rate limits and the quota
Each token gets a quota of requests per wall-clock minute (60 by default). Every authenticated request counts, including failed ones.
The client reads X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset from every response:
print(cops.quota)
This prints QuotaSnapshot(limit=60, remaining=57, reset_at=..., resets_in=41.2, reserve=0).
When the quota runs out, requests wait for the next minute by default. With on_quota_exhausted="raise" they raise a local RateLimitError at once instead.
If you host games, set rate_limit_reserve, for example to 15. Autohost calls run at high priority and can use the last requests of each minute. Everything else stops early, so a burst of profile lookups can never starve room polling.
Fetching the OpenAPI document (cops.open_api_spec()) needs no token and doesn't count toward the quota.
Errors
Every failure raises a subclass of CopsApiError, which carries a stable code:
from copsapi import CopsApiError, NotFoundError, RateLimitError
try:
cops.profiles.get("ghost")
except NotFoundError:
...
except RateLimitError as err:
print(f"retry in {err.retry_after}s")
except CopsApiError as err:
print(err.code, err.status, err)
| Exception | Codes |
|---|---|
AuthenticationError |
missing_token, invalid_token, token_revoked |
PermissionDeniedError |
insufficient_permissions: the token lacks a permission, such as Autohost |
RateLimitError |
rate_limited: wait retry_after seconds |
BadRequestError |
bad_request: a parameter is malformed or out of range |
NotFoundError |
user_not_found, game_not_found |
ServiceUnavailableError |
server_unavailable, api_disabled, internal_error, gateway |
ConnectionFailedError |
network, timeout |
InvalidResponseError |
invalid_response: a 2xx response that isn't JSON |
GameRejectedError |
game_rejected: the game server refused a hosted game |
GameGoneError |
game_gone: a hosted game disappeared |
Other attributes:
err.statusis the HTTP status, or0if there was no response.err.retryabletells you whether trying again later can work.err.localisTruewhen the library refused the request without calling the API, for example on validation or to keep the quota reserve.is_cops_error(err, "user_not_found")checks for a code without importing the classes.
Caching
Responses are cached in memory, with up to 5,000 entries. Default lifetimes:
| Kind | Default TTL |
|---|---|
profile |
5 minutes |
servers |
30 seconds |
leaderboard |
1 minute |
history |
1 minute |
match |
1 hour |
weapons |
2 minutes |
Autohost calls are never cached. Pass fresh=True to any read to skip the cache, change lifetimes with cache_ttls={"profile": 60}, or call cops.clear_cache().
To share a cache between processes, pass any object with get(key) and set(key, value, ttl_seconds), and optionally clear(). The async client also accepts async methods. Cached values are model objects, so a shared store needs to pickle them:
import pickle
import redis
class RedisStore:
def __init__(self, client):
self.client = client
def get(self, key):
data = self.client.get(f"cops:{key}")
return pickle.loads(data) if data else None
def set(self, key, value, ttl):
self.client.set(f"cops:{key}", pickle.dumps(value), ex=max(1, int(ttl)))
cops = CopsClient(cache=RedisStore(redis.Redis()))
Game catalog
The game modes and maps that can be hosted through the API:
from copsapi import GAME_MODES, MAPS, game_mode, map_id, map_name, is_hostable, maps_for, modes_for
game_mode("tournament")
map_id("Castello")
map_name(613)
is_hostable("wingman", "Grounded")
maps_for("tournament")
modes_for("Purify")
game_mode("tournament")returnsGameMode(id=15847, key='tournament', name='Tournament Mode', maps=(...)).map_id("Castello")returns15391, andmap_name(613)returns'Canals'.is_hostable("wingman", "Grounded")returnsFalse.
| Mode | key |
id | Maps |
|---|---|---|---|
| Defuse | defuse |
15773 | Canals, Plaza, Legacy, Grounded, Bureau, Uplink, Yard, Raid, Port, Park, Purify, Arctic, Bout, Soar, Shanty, Village, Castello |
| Elimination | elimination |
15775 | Warehouse, Impasse, Overhaul |
| Gungame | gungame |
15776 | Brewery, Division, Uplink, Yard, Heat, Warehouse, Gallery, Park, Cargo, Arctic, Bout, Hangar, Impasse, Overhaul |
| Team Deathmatch | tdm |
15777 | All 26 maps |
| Manhunt | manhunt |
15846 | Purify |
| Tournament Mode | tournament |
15847 | Canals, Plaza, Legacy, Grounded, Bureau, Raid, Port, Soar, Village, Castello |
| Rush | rush |
15860 | All 26 maps |
| Defend | defend |
15861 | Shanty |
| Manhunt Z | manhuntz |
18596 | Purify |
| Wingman | wingman |
18850 | Canals, Plaza, Legacy, Bureau, Raid, Village, Castello |
The package also exports these label tables:
BAN_REASONS,USER_TYPES,ACCOUNT_AGES,PARTY_INVITE_PERMISSIONSandCLAN_RANKS, which map API codes to readable text.LEADERBOARD_TYPES,MATCH_STATES,GAME_TYPES,INPUT_DEVICESandAUTOHOST_COMMANDS.
Named constants
Every code the API uses has a named constant, so you don't have to remember what 2 or 15847 means:
from copsapi import BanReason, UserType, MapId, GameModeId, LeaderboardType, AutohostCommand, InputDeviceFlag
if profile.ban and profile.ban.reason == BanReason.HACKING:
flag_for_review(profile)
if player.user_type == UserType.MODERATOR:
print("a moderator is in the room")
cops.autohost.create(server_id=server_id, map=MapId.CASTELLO, mode=GameModeId.TOURNAMENT_MODE, room_name="Final")
cops.autohost.control(game_id, AutohostCommand.KICK, user_id)
page = cops.leaderboards.page(LeaderboardType.KILLS)
devices = InputDeviceFlag(player.input_device_flags)
if InputDeviceFlag.TOUCH in devices:
print("plays on a touch screen")
| Constant | Kind | Members |
|---|---|---|
UserType |
IntEnum |
SPECTATOR, USER, ASSISTANT_MOD, MODERATOR, VETERAN_MOD, DEVELOPER |
BanType |
IntEnum |
PERMANENT, TEMPORARY |
BanReason |
IntEnum |
UNKNOWN, FRAUD_ATTEMPT, HACKING, GRIEFING, VERBAL_ABUSE, IDLE, SPAM, REPORT_ABUSE, ACCOUNT_TRADING, GLITCHING, GHOSTING, PARTYING_WITH_HACKER, UNDER_INVESTIGATION, MARKETPLACE_ABUSE |
ClanMemberRank |
IntEnum |
MEMBER, OFFICER, CO_LEADER, LEADER |
PartyInvitePermission |
IntEnum |
ANYBODY, ONLY_FRIENDS_AND_CLAN_MEMBERS, NOBODY |
GameType |
IntEnum |
RANKED, CASUAL |
MatchResult |
IntEnum |
WIN, LOSS, DRAW |
InputDeviceFlag |
IntFlag |
KEYBOARD_MOUSE, TOUCH, GAMEPAD |
MapId |
IntEnum |
Every map, such as CANALS or CASTELLO |
GameModeId |
IntEnum |
Every hostable mode, such as DEFUSE, TEAM_DEATHMATCH or TOURNAMENT_MODE |
AccountAge |
str enum |
UNDER_A_WEEK ("1w") to FOUR_YEARS_OR_MORE ("5y+") |
WinCondition |
str enum |
TEAM_ELIMINATED, TEAM_KILLS, TIME_UP, SURRENDERED |
LeaveReason |
str enum |
LEFT, TIMEOUT, KICKED, BANNED, PING_LIMIT, AFK |
MatchState |
str enum |
WAITING_FOR_PLAYERS, STARTING, FREEZETIME, ON_GOING, BOMB_PLANTED, WIN, TEAM_FLIP, GAME_END, CHANGING_MAP |
AutohostCommand |
str enum |
KICK, RESTART, ROOM_OWNER_ADD, ROOM_OWNER_REMOVE, CLOSE |
LeaderboardType |
str enum |
MMR, KILLS, DEATHS, ASSISTS, MATCHES, WINS, LOSSES, XP, MVP |
GameStatus |
str enum |
OPEN, ENDED |
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.
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.
Helpers
| Helper | Returns |
|---|---|
career_totals(profile, "ranked" | "casual" | "custom" | "all") |
Totals(kills, deaths, assists, wins, losses, matches, kd, win_rate) over all seasons. |
season_totals(profile, season=None, scope="all") |
The same for one season (the latest by default). |
summarize_result(result) |
Teams sorted by kills, the winner, draw, duration, top fragger and leavers, for autohost results or fetched matches. |
describe_ban(ban) |
Text such as "Banned for 3 more days (Griefing)". |
ban_reason(ban) |
The reason label. |
input_devices(flags) |
["keyboard_mouse", "touch", "gamepad"], decoded from input_device_flags. |
account_age_label(profile) |
Text such as "Under 3 months". |
is_new_account(profile) |
True for accounts under a month old. |
is_staff(user_type) |
True for Critical Ops moderators and developers. |
kd(kills, deaths) |
The K/D ratio, rounded. |
random_password(length=6) |
A room password without look-alike characters. |
region_of(server_name) |
"EU", "NA", "SA", "AS", "OC", "ME", "AF" or None, guessed from the name. |
Command line
pip install copsapi-wrapper
export COPS_API_TOKEN=cops_...
copsapi token
copsapi profile 123456 SomePlayer
copsapi servers
copsapi history 123456
copsapi match <match_id>
copsapi weapons 123456 casual
copsapi leaderboard mmr 1 --rank 9
copsapi host --server 6 --mode tournament --map Castello --name "Scrim" --owner 123456
copsapi watch 812
copsapi control 812 kick 654321
copsapi catalog
copsapi quota
Pass your own token with --token cops_..., or set COPS_API_TOKEN. Add --json before the command for JSON output, for example copsapi --json servers. python -m copsapi works too.
Models
Responses are frozen dataclasses. They keep the API's own snake_case field names, exactly as the OpenAPI spec defines them:
Profilehasbasic_info,account_age,ban,stats(seasonal_stats,ranked,leaderboard_data) andclan, plus the shortcuts.user_idand.name.AutohostGamehasstatus,room(code,map_id, ...),players,scores,round,match_stateandresults, plus the shortcuts.openand.code.MatchInfoandAutohostResulthaveplayers,scores,winning_teamandwin_condition.MatchInfoalso has.startedand.endedasdatetime.Profile,MatchInfo,MatchHistoryEntryandAutohostGamekeep the full JSON in.raw, including fields the API adds after this release.
Field reference:
- Times are Unix timestamps in seconds.
teamis-1for a player who hasn't picked a team.pingis-1while the player is disconnected;PlayerMatchStats.connectedchecks this for you.winning_teamis-1when no team won.ranked.rankis0while placement matches are left and9for Elite Ops.ban.days_leftis0for a ban that never ends;Ban.permanenttells you whether the ban is permanent.
Development
python -m venv .venv
.venv/Scripts/pip install -e ".[dev]"
pytest
mypy
On macOS and Linux, use .venv/bin/pip.
The sync client (_sync_client.py) is generated from the async source (_async_client.py). After changing the async client, run python scripts/generate_sync.py; a test fails if the two are out of step.
License
MIT. This is an unofficial client; Critical Ops and the Critical Ops API belong to Critical Force.
Metadata
Release files for copsapi-wrapper 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| copsapi_wrapper-1.1.0.tar.gz | 54.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| copsapi_wrapper-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 106.0 kB
Release files / copsapi_wrapper-1.1.0.tar.gz
| Download URL | copsapi_wrapper-1.1.0.tar.gz |
|---|---|
| Size | 54.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fd8adfa12bd6323ebc8a046452383ccfb7b5ceb316810a6291b76895ffa1d51f
|
|
BLAKE2b-256 checksum How to use checksums |
cc0af57096d48b607b4c4449ae233f260ff5b2afd933c7d2ee90f5e82b323a0e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|
Release files / copsapi_wrapper-1.1.0-py3-none-any.whl
| Download URL | copsapi_wrapper-1.1.0-py3-none-any.whl |
|---|---|
| Size | 51.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bfe8598bb752646c68eb2d3785b4fbaa148149ba8309ace79170153a5b67f644
|
|
BLAKE2b-256 checksum How to use checksums |
7127249cd607140842d7558ed695e577db2ef76b7f4e1c7639ef953759f00237
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|
Security for copsapi-wrapper 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.