wok-api 1.2.0__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.
- wok_api-1.2.0/PKG-INFO +99 -0
- wok_api-1.2.0/README.md +78 -0
- wok_api-1.2.0/pyproject.toml +44 -0
- wok_api-1.2.0/setup.cfg +4 -0
- wok_api-1.2.0/wok_api.egg-info/PKG-INFO +99 -0
- wok_api-1.2.0/wok_api.egg-info/SOURCES.txt +7 -0
- wok_api-1.2.0/wok_api.egg-info/dependency_links.txt +1 -0
- wok_api-1.2.0/wok_api.egg-info/top_level.txt +1 -0
- wok_api-1.2.0/wok_api.py +325 -0
wok_api-1.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wok-api
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Dependency-free synchronous client for the WOK Steam API
|
|
5
|
+
Author: WOK API
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://woksteamapi.com
|
|
8
|
+
Project-URL: Documentation, https://woksteamapi.com/docs
|
|
9
|
+
Keywords: wok-api,steam,steam-api,steam-inventory-api,steamwebapi,cs2,inventory,faceit
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# WOK API client for Python
|
|
23
|
+
|
|
24
|
+
Dependency-free synchronous client for the
|
|
25
|
+
[WOK Steam API](https://woksteamapi.com/docs). It is distributed as the single
|
|
26
|
+
`wok_api` module and uses only the Python standard library at runtime.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Python 3.10 or newer
|
|
31
|
+
- A WOK API key
|
|
32
|
+
|
|
33
|
+
For development, the Free Developer plan provides 1,000 requests per calendar
|
|
34
|
+
month, a 100-request daily guard and 30 requests per minute. Create a key at
|
|
35
|
+
<https://woksteamapi.com/pricing>.
|
|
36
|
+
|
|
37
|
+
Keep the API key in a server-side environment variable or secret store. Do not
|
|
38
|
+
commit it to source control or send it to browser clients.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
python -m pip install wok-api
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import os
|
|
50
|
+
|
|
51
|
+
from wok_api import WokApi, WokApiError
|
|
52
|
+
|
|
53
|
+
client = WokApi(os.environ["WOK_API_KEY"])
|
|
54
|
+
steam_id = os.environ["STEAM_ID"]
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
inventory = client.inventory(
|
|
58
|
+
steam_id,
|
|
59
|
+
game="cs2",
|
|
60
|
+
top=25,
|
|
61
|
+
)
|
|
62
|
+
print(inventory)
|
|
63
|
+
except WokApiError as error:
|
|
64
|
+
print(error.status, error, error.retry_after)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The client also exposes methods for profiles, friends, catalog prices, price
|
|
68
|
+
history, CS2 inspection, crawl jobs, FACEIT data, and service health. See the
|
|
69
|
+
[API reference](https://woksteamapi.com/docs) for request and response shapes.
|
|
70
|
+
|
|
71
|
+
Render a modern CS2 inspect certificate as PNG bytes:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
png = client.cs2_screenshot(
|
|
75
|
+
certificate=os.environ["CS2_INSPECT_CERTIFICATE"],
|
|
76
|
+
theme="wok",
|
|
77
|
+
item_name="AK-47 | Redline (Field-Tested)",
|
|
78
|
+
)
|
|
79
|
+
with open("wok-cs2-inspect.png", "wb") as output:
|
|
80
|
+
output.write(png)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`WokApiError` provides `status`, `payload`, `retry_after`, `rate_limit`,
|
|
84
|
+
`rate_remaining`, and `rate_reset`. Wait at least `retry_after` seconds after
|
|
85
|
+
HTTP 429. Network failures use status `0`.
|
|
86
|
+
|
|
87
|
+
The default timeout is 35 seconds so the 25-second synchronous inventory batch
|
|
88
|
+
can finish. Do not configure `/v1/inventories` with a timeout below 30 seconds.
|
|
89
|
+
|
|
90
|
+
For testing or a compatible deployment, pass `base_url`, `timeout`, or a custom
|
|
91
|
+
`transport` to the constructor.
|
|
92
|
+
|
|
93
|
+
## Build the package
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
python -m build
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The command creates both an sdist and a wheel under `dist/`.
|
wok_api-1.2.0/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# WOK API client for Python
|
|
2
|
+
|
|
3
|
+
Dependency-free synchronous client for the
|
|
4
|
+
[WOK Steam API](https://woksteamapi.com/docs). It is distributed as the single
|
|
5
|
+
`wok_api` module and uses only the Python standard library at runtime.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python 3.10 or newer
|
|
10
|
+
- A WOK API key
|
|
11
|
+
|
|
12
|
+
For development, the Free Developer plan provides 1,000 requests per calendar
|
|
13
|
+
month, a 100-request daily guard and 30 requests per minute. Create a key at
|
|
14
|
+
<https://woksteamapi.com/pricing>.
|
|
15
|
+
|
|
16
|
+
Keep the API key in a server-side environment variable or secret store. Do not
|
|
17
|
+
commit it to source control or send it to browser clients.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
python -m pip install wok-api
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
from wok_api import WokApi, WokApiError
|
|
31
|
+
|
|
32
|
+
client = WokApi(os.environ["WOK_API_KEY"])
|
|
33
|
+
steam_id = os.environ["STEAM_ID"]
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
inventory = client.inventory(
|
|
37
|
+
steam_id,
|
|
38
|
+
game="cs2",
|
|
39
|
+
top=25,
|
|
40
|
+
)
|
|
41
|
+
print(inventory)
|
|
42
|
+
except WokApiError as error:
|
|
43
|
+
print(error.status, error, error.retry_after)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The client also exposes methods for profiles, friends, catalog prices, price
|
|
47
|
+
history, CS2 inspection, crawl jobs, FACEIT data, and service health. See the
|
|
48
|
+
[API reference](https://woksteamapi.com/docs) for request and response shapes.
|
|
49
|
+
|
|
50
|
+
Render a modern CS2 inspect certificate as PNG bytes:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
png = client.cs2_screenshot(
|
|
54
|
+
certificate=os.environ["CS2_INSPECT_CERTIFICATE"],
|
|
55
|
+
theme="wok",
|
|
56
|
+
item_name="AK-47 | Redline (Field-Tested)",
|
|
57
|
+
)
|
|
58
|
+
with open("wok-cs2-inspect.png", "wb") as output:
|
|
59
|
+
output.write(png)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`WokApiError` provides `status`, `payload`, `retry_after`, `rate_limit`,
|
|
63
|
+
`rate_remaining`, and `rate_reset`. Wait at least `retry_after` seconds after
|
|
64
|
+
HTTP 429. Network failures use status `0`.
|
|
65
|
+
|
|
66
|
+
The default timeout is 35 seconds so the 25-second synchronous inventory batch
|
|
67
|
+
can finish. Do not configure `/v1/inventories` with a timeout below 30 seconds.
|
|
68
|
+
|
|
69
|
+
For testing or a compatible deployment, pass `base_url`, `timeout`, or a custom
|
|
70
|
+
`transport` to the constructor.
|
|
71
|
+
|
|
72
|
+
## Build the package
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
python -m build
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The command creates both an sdist and a wheel under `dist/`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wok-api"
|
|
7
|
+
version = "1.2.0"
|
|
8
|
+
description = "Dependency-free synchronous client for the WOK Steam API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "WOK API" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"wok-api",
|
|
17
|
+
"steam",
|
|
18
|
+
"steam-api",
|
|
19
|
+
"steam-inventory-api",
|
|
20
|
+
"steamwebapi",
|
|
21
|
+
"cs2",
|
|
22
|
+
"inventory",
|
|
23
|
+
"faceit",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
]
|
|
36
|
+
dependencies = []
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://woksteamapi.com"
|
|
40
|
+
Documentation = "https://woksteamapi.com/docs"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools]
|
|
43
|
+
py-modules = ["wok_api"]
|
|
44
|
+
include-package-data = false
|
wok_api-1.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wok-api
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Dependency-free synchronous client for the WOK Steam API
|
|
5
|
+
Author: WOK API
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://woksteamapi.com
|
|
8
|
+
Project-URL: Documentation, https://woksteamapi.com/docs
|
|
9
|
+
Keywords: wok-api,steam,steam-api,steam-inventory-api,steamwebapi,cs2,inventory,faceit
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# WOK API client for Python
|
|
23
|
+
|
|
24
|
+
Dependency-free synchronous client for the
|
|
25
|
+
[WOK Steam API](https://woksteamapi.com/docs). It is distributed as the single
|
|
26
|
+
`wok_api` module and uses only the Python standard library at runtime.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Python 3.10 or newer
|
|
31
|
+
- A WOK API key
|
|
32
|
+
|
|
33
|
+
For development, the Free Developer plan provides 1,000 requests per calendar
|
|
34
|
+
month, a 100-request daily guard and 30 requests per minute. Create a key at
|
|
35
|
+
<https://woksteamapi.com/pricing>.
|
|
36
|
+
|
|
37
|
+
Keep the API key in a server-side environment variable or secret store. Do not
|
|
38
|
+
commit it to source control or send it to browser clients.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
python -m pip install wok-api
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import os
|
|
50
|
+
|
|
51
|
+
from wok_api import WokApi, WokApiError
|
|
52
|
+
|
|
53
|
+
client = WokApi(os.environ["WOK_API_KEY"])
|
|
54
|
+
steam_id = os.environ["STEAM_ID"]
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
inventory = client.inventory(
|
|
58
|
+
steam_id,
|
|
59
|
+
game="cs2",
|
|
60
|
+
top=25,
|
|
61
|
+
)
|
|
62
|
+
print(inventory)
|
|
63
|
+
except WokApiError as error:
|
|
64
|
+
print(error.status, error, error.retry_after)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The client also exposes methods for profiles, friends, catalog prices, price
|
|
68
|
+
history, CS2 inspection, crawl jobs, FACEIT data, and service health. See the
|
|
69
|
+
[API reference](https://woksteamapi.com/docs) for request and response shapes.
|
|
70
|
+
|
|
71
|
+
Render a modern CS2 inspect certificate as PNG bytes:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
png = client.cs2_screenshot(
|
|
75
|
+
certificate=os.environ["CS2_INSPECT_CERTIFICATE"],
|
|
76
|
+
theme="wok",
|
|
77
|
+
item_name="AK-47 | Redline (Field-Tested)",
|
|
78
|
+
)
|
|
79
|
+
with open("wok-cs2-inspect.png", "wb") as output:
|
|
80
|
+
output.write(png)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`WokApiError` provides `status`, `payload`, `retry_after`, `rate_limit`,
|
|
84
|
+
`rate_remaining`, and `rate_reset`. Wait at least `retry_after` seconds after
|
|
85
|
+
HTTP 429. Network failures use status `0`.
|
|
86
|
+
|
|
87
|
+
The default timeout is 35 seconds so the 25-second synchronous inventory batch
|
|
88
|
+
can finish. Do not configure `/v1/inventories` with a timeout below 30 seconds.
|
|
89
|
+
|
|
90
|
+
For testing or a compatible deployment, pass `base_url`, `timeout`, or a custom
|
|
91
|
+
`transport` to the constructor.
|
|
92
|
+
|
|
93
|
+
## Build the package
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
python -m build
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The command creates both an sdist and a wheel under `dist/`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wok_api
|
wok_api-1.2.0/wok_api.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Небольшой синхронный клиент WOK без сторонних зависимостей."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Callable, Iterable
|
|
7
|
+
from urllib.error import HTTPError, URLError
|
|
8
|
+
from urllib.parse import quote, urlencode, urlparse
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WokApiError(RuntimeError):
|
|
13
|
+
"""Ошибка API с сохранёнными кодом ответа и безопасным JSON-телом."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, status: int, message: str, *,
|
|
16
|
+
payload: Any = None, retry_after: str | None = None,
|
|
17
|
+
rate_limit: str | None = None,
|
|
18
|
+
rate_remaining: str | None = None,
|
|
19
|
+
rate_reset: str | None = None):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.status = status
|
|
22
|
+
self.payload = payload
|
|
23
|
+
self.retry_after = retry_after
|
|
24
|
+
self.rate_limit = rate_limit
|
|
25
|
+
self.rate_remaining = rate_remaining
|
|
26
|
+
self.rate_reset = rate_reset
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Transport = Callable[[Request, float], Any]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WokApi:
|
|
33
|
+
"""Клиент публичных маршрутов WOK Steam API."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, api_key: str, *,
|
|
36
|
+
base_url: str = "https://woksteamapi.com",
|
|
37
|
+
timeout: float = 35.0,
|
|
38
|
+
transport: Transport | None = None):
|
|
39
|
+
parsed = urlparse(base_url)
|
|
40
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
41
|
+
raise ValueError("base_url должен быть абсолютным HTTP(S) URL")
|
|
42
|
+
if not isinstance(api_key, str) or not api_key.strip():
|
|
43
|
+
raise ValueError("api_key не должен быть пустым")
|
|
44
|
+
if timeout <= 0:
|
|
45
|
+
raise ValueError("timeout должен быть больше нуля")
|
|
46
|
+
self.base_url = base_url.rstrip("/")
|
|
47
|
+
self.timeout = float(timeout)
|
|
48
|
+
self._api_key = api_key.strip()
|
|
49
|
+
self._transport = transport or self._open
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _open(request: Request, timeout: float):
|
|
53
|
+
return urlopen(request, timeout=timeout)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _decode(raw: bytes) -> Any:
|
|
57
|
+
if not raw:
|
|
58
|
+
return None
|
|
59
|
+
try:
|
|
60
|
+
return json.loads(raw.decode("utf-8"))
|
|
61
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
62
|
+
return {"error": "invalid_json_response"}
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _error_message(payload: Any) -> str:
|
|
66
|
+
"""Извлечь сообщение и из простого, и из структурного error."""
|
|
67
|
+
if not isinstance(payload, dict):
|
|
68
|
+
return "API request failed"
|
|
69
|
+
message = payload.get("message")
|
|
70
|
+
if isinstance(message, str) and message:
|
|
71
|
+
return message
|
|
72
|
+
error = payload.get("error")
|
|
73
|
+
if isinstance(error, str) and error:
|
|
74
|
+
return error
|
|
75
|
+
if isinstance(error, dict):
|
|
76
|
+
for field in ("message", "code"):
|
|
77
|
+
value = error.get(field)
|
|
78
|
+
if isinstance(value, str) and value:
|
|
79
|
+
return value
|
|
80
|
+
return "API request failed"
|
|
81
|
+
|
|
82
|
+
def _request(self, method: str, path: str, *,
|
|
83
|
+
query: dict[str, Any] | None = None,
|
|
84
|
+
body: dict[str, Any] | None = None,
|
|
85
|
+
authenticated: bool = True) -> Any:
|
|
86
|
+
if not path.startswith("/"):
|
|
87
|
+
raise ValueError("path должен начинаться с /")
|
|
88
|
+
clean_query = {
|
|
89
|
+
key: value for key, value in (query or {}).items()
|
|
90
|
+
if value is not None and value != ""
|
|
91
|
+
}
|
|
92
|
+
suffix = f"?{urlencode(clean_query, doseq=True)}" if clean_query else ""
|
|
93
|
+
raw_body = (json.dumps(body, separators=(",", ":")).encode("utf-8")
|
|
94
|
+
if body is not None else None)
|
|
95
|
+
headers = {
|
|
96
|
+
"Accept": "application/json",
|
|
97
|
+
"User-Agent": "wok-api-python/1.2.0",
|
|
98
|
+
}
|
|
99
|
+
if authenticated:
|
|
100
|
+
headers["Authorization"] = f"Bearer {self._api_key}"
|
|
101
|
+
if raw_body is not None:
|
|
102
|
+
headers["Content-Type"] = "application/json"
|
|
103
|
+
request = Request(
|
|
104
|
+
f"{self.base_url}{path}{suffix}", data=raw_body,
|
|
105
|
+
headers=headers, method=method,
|
|
106
|
+
)
|
|
107
|
+
try:
|
|
108
|
+
with self._transport(request, self.timeout) as response:
|
|
109
|
+
return self._decode(response.read())
|
|
110
|
+
except HTTPError as exc:
|
|
111
|
+
payload = self._decode(exc.read())
|
|
112
|
+
raise WokApiError(
|
|
113
|
+
int(exc.code), self._error_message(payload), payload=payload,
|
|
114
|
+
retry_after=exc.headers.get("Retry-After"),
|
|
115
|
+
rate_limit=exc.headers.get("X-RateLimit-Limit"),
|
|
116
|
+
rate_remaining=exc.headers.get("X-RateLimit-Remaining"),
|
|
117
|
+
rate_reset=exc.headers.get("X-RateLimit-Reset"),
|
|
118
|
+
) from exc
|
|
119
|
+
except URLError as exc:
|
|
120
|
+
raise WokApiError(0, "Network request failed") from exc
|
|
121
|
+
|
|
122
|
+
def _request_bytes(self, method: str, path: str, *,
|
|
123
|
+
body: dict[str, Any]) -> bytes:
|
|
124
|
+
"""Request a bounded binary API response while preserving JSON errors."""
|
|
125
|
+
raw_body = json.dumps(body, separators=(",", ":")).encode("utf-8")
|
|
126
|
+
request = Request(
|
|
127
|
+
f"{self.base_url}{path}", data=raw_body,
|
|
128
|
+
headers={
|
|
129
|
+
"Accept": "image/png",
|
|
130
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
"User-Agent": "wok-api-python/1.2.0",
|
|
133
|
+
},
|
|
134
|
+
method=method,
|
|
135
|
+
)
|
|
136
|
+
try:
|
|
137
|
+
with self._transport(request, self.timeout) as response:
|
|
138
|
+
return response.read()
|
|
139
|
+
except HTTPError as exc:
|
|
140
|
+
payload = self._decode(exc.read())
|
|
141
|
+
raise WokApiError(
|
|
142
|
+
int(exc.code), self._error_message(payload), payload=payload,
|
|
143
|
+
retry_after=exc.headers.get("Retry-After"),
|
|
144
|
+
rate_limit=exc.headers.get("X-RateLimit-Limit"),
|
|
145
|
+
rate_remaining=exc.headers.get("X-RateLimit-Remaining"),
|
|
146
|
+
rate_reset=exc.headers.get("X-RateLimit-Reset"),
|
|
147
|
+
) from exc
|
|
148
|
+
except URLError as exc:
|
|
149
|
+
raise WokApiError(0, "Network request failed") from exc
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _values(values: Iterable[str], name: str) -> list[str]:
|
|
153
|
+
if isinstance(values, (str, bytes)):
|
|
154
|
+
raise ValueError(f"{name} должен быть списком")
|
|
155
|
+
result = [str(value) for value in values]
|
|
156
|
+
if not result or any(not value for value in result):
|
|
157
|
+
raise ValueError(f"{name} не должен быть пустым")
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
def inventory(self, steam_id: str, *, game: str = "cs2",
|
|
161
|
+
no_cache: bool = False, top: int = 10,
|
|
162
|
+
include_inspect: bool = False,
|
|
163
|
+
refresh_prices: bool = False,
|
|
164
|
+
price_source: str | None = None,
|
|
165
|
+
strict: bool = False) -> dict[str, Any]:
|
|
166
|
+
return self._request("GET", "/v1/inventory", query={
|
|
167
|
+
"steam_id": steam_id, "game": game,
|
|
168
|
+
"no_cache": int(no_cache), "top": top,
|
|
169
|
+
"include_inspect": int(include_inspect),
|
|
170
|
+
"refresh_prices": 1 if refresh_prices else None,
|
|
171
|
+
"price_source": price_source,
|
|
172
|
+
"strict": int(strict),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
def refresh_inventory_prices(self, steam_id: str, *, game: str = "cs2",
|
|
176
|
+
top: int = 10,
|
|
177
|
+
include_inspect: bool = False,
|
|
178
|
+
price_source: str | None = None,
|
|
179
|
+
strict: bool = False) -> dict[str, Any]:
|
|
180
|
+
"""Refresh prices from the local cache without fetching Steam inventory."""
|
|
181
|
+
body = {
|
|
182
|
+
"steam_id": steam_id,
|
|
183
|
+
"game": game,
|
|
184
|
+
"top": int(top),
|
|
185
|
+
"include_inspect": bool(include_inspect),
|
|
186
|
+
"strict": bool(strict),
|
|
187
|
+
}
|
|
188
|
+
if price_source is not None:
|
|
189
|
+
body["price_source"] = price_source
|
|
190
|
+
return self._request("POST", "/v1/inventory/refresh-prices", body=body)
|
|
191
|
+
|
|
192
|
+
def inventories(self, steamids: Iterable[str], *,
|
|
193
|
+
games: Iterable[str] = ("cs2",),
|
|
194
|
+
no_cache: bool = False, top: int = 10,
|
|
195
|
+
include_inspect: bool = False) -> dict[str, Any]:
|
|
196
|
+
return self._request("POST", "/v1/inventories", body={
|
|
197
|
+
"steamids": self._values(steamids, "steamids"),
|
|
198
|
+
"games": self._values(games, "games"),
|
|
199
|
+
"no_cache": bool(no_cache), "top": int(top),
|
|
200
|
+
"include_inspect": bool(include_inspect),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
def compatibility_inventory(self, steam_id: str, *, game: str = "cs2",
|
|
204
|
+
no_cache: bool = False,
|
|
205
|
+
grouped: bool = True,
|
|
206
|
+
refresh_prices: bool = False,
|
|
207
|
+
price_source: str | None = None,
|
|
208
|
+
strict: bool = False) -> list[dict[str, Any]]:
|
|
209
|
+
return self._request("GET", "/steam/api/inventory", query={
|
|
210
|
+
"steam_id": steam_id, "game": game, "currency": "USD",
|
|
211
|
+
"no_cache": int(no_cache), "group": int(grouped),
|
|
212
|
+
"refresh_prices": 1 if refresh_prices else None,
|
|
213
|
+
"price_source": price_source,
|
|
214
|
+
"strict": int(strict),
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
def compatibility_profile(self, steam_id: str) -> dict[str, Any]:
|
|
218
|
+
return self._request("GET", "/steam/api/profile", query={
|
|
219
|
+
"steam_id": steam_id,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
def profile(self, steam_id: str) -> dict[str, Any]:
|
|
223
|
+
return self._request("GET", "/v1/profile", query={"steam_id": steam_id})
|
|
224
|
+
|
|
225
|
+
def profiles(self, steamids: Iterable[str]) -> dict[str, dict[str, Any]]:
|
|
226
|
+
return self._request("POST", "/v1/profiles", body={
|
|
227
|
+
"steamids": self._values(steamids, "steamids"),
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
def player_security(self, steam_id: str) -> dict[str, Any]:
|
|
231
|
+
return self._request("GET", "/v1/player-security", query={
|
|
232
|
+
"steam_id": steam_id,
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
def friends(self, steam_id: str, *, limit: int = 100,
|
|
236
|
+
cursor: str | None = None) -> dict[str, Any]:
|
|
237
|
+
return self._request("GET", "/v1/friends", query={
|
|
238
|
+
"steam_id": steam_id, "limit": limit, "cursor": cursor,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
def price(self, game: str, name: str) -> dict[str, Any]:
|
|
242
|
+
return self._request("GET", "/v1/price", query={
|
|
243
|
+
"game": game, "name": name,
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
def prices(self, game: str, names: Iterable[str]) -> dict[str, Any]:
|
|
247
|
+
return self._request("POST", "/v1/prices", body={
|
|
248
|
+
"game": game, "names": self._values(names, "names"),
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
def items(self, *, game: str = "cs2", query: str | None = None,
|
|
252
|
+
source: str | None = None, limit: int = 50,
|
|
253
|
+
cursor: str | None = None) -> dict[str, Any]:
|
|
254
|
+
return self._request("GET", "/v1/items", query={
|
|
255
|
+
"game": game, "q": query, "source": source,
|
|
256
|
+
"limit": limit, "cursor": cursor,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
def price_history(self, game: str, name: str, *, days: int = 30,
|
|
260
|
+
source: str | None = None) -> dict[str, Any]:
|
|
261
|
+
return self._request("GET", "/v1/price-history", query={
|
|
262
|
+
"game": game, "name": name, "days": days, "source": source,
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
def inspect_cs2(self, inspect_link: str) -> dict[str, Any]:
|
|
266
|
+
"""float_value читать по float, paint_seed и paint_index по их флагам."""
|
|
267
|
+
return self._request("POST", "/v1/cs2/inspect", body={
|
|
268
|
+
"inspect_link": inspect_link,
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
def cs2_screenshot(self, inspect_link: str | None = None, *,
|
|
272
|
+
certificate: str | None = None,
|
|
273
|
+
theme: str = "wok",
|
|
274
|
+
item_name: str | None = None,
|
|
275
|
+
download: bool = False) -> bytes:
|
|
276
|
+
"""Render the modern inspect certificate locally on WOK as PNG."""
|
|
277
|
+
if bool(inspect_link) == bool(certificate):
|
|
278
|
+
raise ValueError(
|
|
279
|
+
"передайте ровно одно поле: inspect_link или certificate")
|
|
280
|
+
body: dict[str, Any] = {
|
|
281
|
+
"theme": theme,
|
|
282
|
+
"download": bool(download),
|
|
283
|
+
}
|
|
284
|
+
if inspect_link:
|
|
285
|
+
body["inspect_link"] = inspect_link
|
|
286
|
+
if certificate:
|
|
287
|
+
body["certificate"] = certificate
|
|
288
|
+
if item_name:
|
|
289
|
+
body["item_name"] = item_name
|
|
290
|
+
return self._request_bytes("POST", "/v1/cs2/screenshot", body=body)
|
|
291
|
+
|
|
292
|
+
def create_crawl(self, steamids: Iterable[str], *,
|
|
293
|
+
games: Iterable[str] = ("cs2",)) -> dict[str, Any]:
|
|
294
|
+
return self._request("POST", "/v1/crawl", body={
|
|
295
|
+
"steamids": self._values(steamids, "steamids"),
|
|
296
|
+
"games": self._values(games, "games"),
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
def crawl_status(self, batch_id: str) -> dict[str, Any]:
|
|
300
|
+
return self._request("GET", f"/v1/crawl/{quote(batch_id, safe='')}")
|
|
301
|
+
|
|
302
|
+
def faceit_player(self, steam_id: str, *, game: str = "cs2") -> dict[str, Any]:
|
|
303
|
+
return self._request("GET", "/faceit/data/v4/players", query={
|
|
304
|
+
"game": game, "game_player_id": steam_id,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
def faceit_stats(self, player_id: str, *, game: str = "cs2") -> dict[str, Any]:
|
|
308
|
+
player = quote(player_id, safe="")
|
|
309
|
+
game_name = quote(game, safe="")
|
|
310
|
+
return self._request(
|
|
311
|
+
"GET", f"/faceit/data/v4/players/{player}/stats/{game_name}")
|
|
312
|
+
|
|
313
|
+
def faceit_history(self, player_id: str, *, game: str = "cs2",
|
|
314
|
+
limit: int = 5) -> dict[str, Any]:
|
|
315
|
+
player = quote(player_id, safe="")
|
|
316
|
+
return self._request(
|
|
317
|
+
"GET", f"/faceit/data/v4/players/{player}/history",
|
|
318
|
+
query={"game": game, "limit": limit},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def health(self) -> dict[str, Any]:
|
|
322
|
+
return self._request("GET", "/healthz", authenticated=False)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
__all__ = ["WokApi", "WokApiError"]
|