skinapi-sdk 0.1.0__py3-none-any.whl

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.
skinapi/__init__.py ADDED
@@ -0,0 +1,175 @@
1
+ """
2
+ skinapi - Python client for SkinAPI.
3
+
4
+ Real-time CS2/Dota2/Rust/TF2 skin prices, float values, inventories,
5
+ profiles and marketplace data.
6
+
7
+ Docs: https://skinapi.skinvaults.online/docs
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import random
13
+ import time
14
+ from typing import Any, Optional
15
+
16
+ import requests
17
+
18
+ __version__ = "0.1.0"
19
+ __all__ = ["SkinAPI", "SkinAPIError"]
20
+
21
+ DEFAULT_BASE = "https://skinapi.skinvaults.online/api/v1"
22
+
23
+
24
+ def _int(v: Optional[str]) -> Optional[int]:
25
+ try:
26
+ return int(v) if v not in (None, "") else None
27
+ except (TypeError, ValueError):
28
+ return None
29
+
30
+
31
+ class SkinAPIError(Exception):
32
+ """Raised on a non-2xx response or network failure."""
33
+
34
+ def __init__(self, status: int, code: Optional[str] = None,
35
+ message: Optional[str] = None, retryable: bool = False):
36
+ super().__init__(message or f"SkinAPI error {status}")
37
+ self.status = status
38
+ self.code = code
39
+ self.retryable = retryable
40
+
41
+
42
+ class SkinAPI:
43
+ """SkinAPI client. Pass your key (sk_live_...), created in the dashboard."""
44
+
45
+ def __init__(self, api_key: str, *, base_url: str = DEFAULT_BASE,
46
+ timeout: float = 15.0, max_retries: int = 2,
47
+ session: Optional[requests.Session] = None):
48
+ if not api_key:
49
+ raise ValueError("api_key is required")
50
+ self.api_key = api_key
51
+ self.base_url = base_url.rstrip("/")
52
+ self.timeout = timeout
53
+ self.max_retries = max_retries
54
+ self.session = session or requests.Session()
55
+ self.session.headers.update({"x-api-key": api_key})
56
+ #: Rate-limit info from the most recent response, or None.
57
+ self.rate_limit: Optional[dict] = None
58
+
59
+ # -- internals --
60
+ def _backoff(self, attempt: int) -> float:
61
+ return (2 ** attempt) * 0.5 * (0.8 + random.random() * 0.4)
62
+
63
+ def _retry_after(self, resp: requests.Response) -> Optional[float]:
64
+ ra = resp.headers.get("retry-after")
65
+ if ra and ra.isdigit():
66
+ return float(ra)
67
+ reset = _int(resp.headers.get("X-RateLimit-Reset"))
68
+ if reset:
69
+ ms = (reset - time.time() * 1000) if reset > 1e12 else (reset - time.time())
70
+ if 0 < ms < 60:
71
+ return ms
72
+ return None
73
+
74
+ def _request(self, method: str, path: str, *, params: Optional[dict] = None,
75
+ json: Optional[dict] = None) -> Any:
76
+ url = self.base_url + path
77
+ if params:
78
+ params = {k: v for k, v in params.items() if v is not None}
79
+ attempt = 0
80
+ while True:
81
+ try:
82
+ resp = self.session.request(method, url, params=params, json=json,
83
+ timeout=self.timeout)
84
+ except requests.RequestException as e:
85
+ if attempt < self.max_retries:
86
+ time.sleep(self._backoff(attempt)); attempt += 1; continue
87
+ raise SkinAPIError(0, "network_error", str(e), retryable=True)
88
+
89
+ self.rate_limit = {
90
+ "remaining_minute": _int(resp.headers.get("X-RateLimit-Remaining-Minute")),
91
+ "remaining_day": _int(resp.headers.get("X-RateLimit-Remaining-Day")),
92
+ "reset": _int(resp.headers.get("X-RateLimit-Reset")),
93
+ }
94
+
95
+ if (resp.status_code == 429 or resp.status_code >= 500) and attempt < self.max_retries:
96
+ time.sleep(self._retry_after(resp) or self._backoff(attempt))
97
+ attempt += 1
98
+ continue
99
+
100
+ try:
101
+ data = resp.json()
102
+ except ValueError:
103
+ data = {}
104
+ if not resp.ok:
105
+ err = (data or {}).get("error", {}) or {}
106
+ raise SkinAPIError(resp.status_code, err.get("code"), err.get("message"),
107
+ retryable=resp.status_code == 429 or resp.status_code >= 500)
108
+ return data.get("data", data)
109
+
110
+ def get(self, path: str, **params) -> Any:
111
+ return self._request("GET", path, params=params)
112
+
113
+ def post(self, path: str, body: dict) -> Any:
114
+ return self._request("POST", path, json=body)
115
+
116
+ # -- prices --
117
+ def item_price(self, name: str, *, game: str = "cs2", currency: str = "USD") -> Any:
118
+ return self.get("/items", name=name, game=game, currency=currency)
119
+
120
+ def item_meta(self, name: str) -> Any:
121
+ return self.get("/items/meta", name=name)
122
+
123
+ def search(self, q: str, *, limit: int = 20) -> Any:
124
+ return self.get("/items/search", q=q, limit=limit)
125
+
126
+ def catalog(self, *, game: str = "cs2", limit: Optional[int] = None,
127
+ offset: Optional[int] = None, q: Optional[str] = None,
128
+ type: Optional[str] = None, rarity: Optional[str] = None,
129
+ prices: Optional[bool] = None, sort: Optional[str] = None,
130
+ currency: str = "USD") -> Any:
131
+ return self.get("/catalog", game=game, limit=limit, offset=offset, q=q,
132
+ type=type, rarity=rarity, prices=prices, sort=sort, currency=currency)
133
+
134
+ def history(self, name: str, *, game: str = "cs2", days: int = 30, currency: str = "USD") -> Any:
135
+ return self.get("/history", name=name, game=game, days=days, currency=currency)
136
+
137
+ # -- marketplaces --
138
+ def markets(self, name: str, *, game: str = "cs2", currency: str = "USD") -> Any:
139
+ return self.get("/markets", name=name, game=game, currency=currency)
140
+
141
+ def markets_batch(self, names: list[str], *, game: str = "cs2", currency: str = "USD") -> Any:
142
+ return self.post("/markets/batch", {"names": names, "game": game, "currency": currency})
143
+
144
+ def deals(self, *, game: str = "cs2", min_discount: Optional[int] = None, currency: str = "USD") -> Any:
145
+ return self.get("/deals", game=game, min_discount=min_discount, currency=currency)
146
+
147
+ # -- floats --
148
+ def float(self, inspect_url: str) -> Any:
149
+ return self.get("/float", url=inspect_url)
150
+
151
+ def float_leaderboard(self, name: str, *, order: str = "asc", limit: int = 10) -> Any:
152
+ return self.get("/float/leaderboard", name=name, order=order, limit=limit)
153
+
154
+ # -- steam --
155
+ def profile(self, id_or_vanity: str) -> Any:
156
+ return self.get("/profile", id=id_or_vanity)
157
+
158
+ def friendlist(self, steam_id: str) -> Any:
159
+ return self.get("/friendlist", steam_id=steam_id)
160
+
161
+ def inventory(self, steam_id: str, *, game: str = "cs2", prices: bool = True, currency: str = "USD") -> Any:
162
+ return self.get("/inventory", steam_id=steam_id, game=game, prices=prices, currency=currency)
163
+
164
+ def inventory_history(self, steam_id: str, *, game: str = "cs2", days: int = 30, currency: str = "USD") -> Any:
165
+ return self.get("/inventory/history", steam_id=steam_id, game=game, days=days, currency=currency)
166
+
167
+ def inventory_batch(self, steam_ids: list[str], *, game: str = "cs2") -> Any:
168
+ return self.post("/inventory/batch", {"steam_ids": steam_ids, "game": game})
169
+
170
+ # -- tools --
171
+ def tradeup(self, items: list[dict]) -> Any:
172
+ return self.post("/tradeup", {"items": items})
173
+
174
+ def status(self) -> Any:
175
+ return self.get("/status")
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: skinapi-sdk
3
+ Version: 0.1.0
4
+ Summary: Python client for SkinAPI - CS2/Steam skin prices, float values, inventories and marketplace data.
5
+ Project-URL: Homepage, https://skinapi.skinvaults.online
6
+ Project-URL: Documentation, https://skinapi.skinvaults.online/docs
7
+ Project-URL: Repository, https://github.com/soccervortex/skinapi-py
8
+ Author: SkinVaults
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,cs2,csgo,float,inventory,marketplace,skin-prices,skinapi,skins,steam
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.8
18
+ Requires-Dist: requests>=2.20
19
+ Description-Content-Type: text/markdown
20
+
21
+ # skinapi (Python)
22
+
23
+ Python client for [SkinAPI](https://skinapi.skinvaults.online) - real-time **CS2 / Dota 2 / Rust / TF2** skin prices, **float values**, Steam **inventories & profiles**, and multi-marketplace data through one REST API.
24
+
25
+ - Real-time prices aggregated across marketplaces
26
+ - CS2 float values, paint seed and wear
27
+ - Steam inventories, profiles and price history
28
+ - Automatic retries, timeouts and rate-limit tracking
29
+ - Free tier - get a key in ~2 minutes
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install skinapi
35
+ ```
36
+
37
+ ## Get a key
38
+
39
+ 1. Sign in with Steam: https://skinapi.skinvaults.online/api/auth/steam
40
+ 2. Create a key in your [dashboard](https://skinapi.skinvaults.online/dashboard)
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ from skinapi import SkinAPI
46
+
47
+ api = SkinAPI("sk_live_your_key")
48
+
49
+ # Aggregated price for one item
50
+ price = api.item_price("AK-47 | Redline (Field-Tested)", game="cs2", currency="USD")
51
+
52
+ # Compare across marketplaces
53
+ markets = api.markets("AK-47 | Redline (Field-Tested)", game="cs2")
54
+
55
+ # Float value, paint seed and wear from a Steam inspect link
56
+ fl = api.float("STEAM_INSPECT_LINK")
57
+
58
+ # Steam profile and bans
59
+ profile = api.profile("STEAM_ID_OR_VANITY")
60
+
61
+ # Priced Steam inventory
62
+ inv = api.inventory("STEAM_ID", game="cs2", prices=True)
63
+
64
+ # Many items at once
65
+ batch = api.markets_batch(["AK-47 | Redline (Field-Tested)", "AWP | Asiimov (Field-Tested)"])
66
+
67
+ # Remaining quota after any call
68
+ print(api.rate_limit) # {'remaining_minute': ..., 'remaining_day': ..., 'reset': ...}
69
+ ```
70
+
71
+ Errors raise `SkinAPIError` with `.status` and `.code`:
72
+
73
+ ```python
74
+ from skinapi import SkinAPIError
75
+
76
+ try:
77
+ api.item_price("Nonexistent Item")
78
+ except SkinAPIError as e:
79
+ print(e.status, e.code, e)
80
+ ```
81
+
82
+ ## Methods
83
+
84
+ `item_price`, `item_meta`, `search`, `catalog`, `history`, `markets`, `markets_batch`,
85
+ `deals`, `float`, `float_leaderboard`, `profile`, `friendlist`, `inventory`,
86
+ `inventory_history`, `inventory_batch`, `tradeup`, `status`.
87
+
88
+ Authentication uses the `x-api-key` header. Games: `cs2`, `dota2`, `rust`, `tf2`.
89
+
90
+ ## Links
91
+
92
+ - Docs: https://skinapi.skinvaults.online/docs
93
+ - OpenAPI spec: https://skinapi.skinvaults.online/api/v1/openapi.json
94
+ - JavaScript client: https://www.npmjs.com/package/skinapi-js
95
+ - Discord: https://discord.gg/CqVnGdGc4Q
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1,5 @@
1
+ skinapi/__init__.py,sha256=rJC__NWcYA5O756hDqGYfp_SkYGxm396srBdZiK4vKA,7166
2
+ skinapi_sdk-0.1.0.dist-info/METADATA,sha256=R5IGzJDCbXqj16hpbY4PGvkWU7UQbAMyWxOgNRSl6qA,3102
3
+ skinapi_sdk-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
4
+ skinapi_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=gZaEVahRmqEN8df_8eQ4AAf5AQCqhF_aqCNPX5OL4-w,1067
5
+ skinapi_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SkinVaults
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.