portals-api 0.1.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.
- portals_api-0.1.0/LICENSE +21 -0
- portals_api-0.1.0/PKG-INFO +218 -0
- portals_api-0.1.0/README.md +188 -0
- portals_api-0.1.0/pyproject.toml +79 -0
- portals_api-0.1.0/setup.cfg +4 -0
- portals_api-0.1.0/src/portals/__init__.py +40 -0
- portals_api-0.1.0/src/portals/_contract.py +2 -0
- portals_api-0.1.0/src/portals/_models.py +71 -0
- portals_api-0.1.0/src/portals/_transport.py +293 -0
- portals_api-0.1.0/src/portals/_version.py +5 -0
- portals_api-0.1.0/src/portals/client.py +197 -0
- portals_api-0.1.0/src/portals/exceptions.py +69 -0
- portals_api-0.1.0/src/portals/models.py +736 -0
- portals_api-0.1.0/src/portals/py.typed +0 -0
- portals_api-0.1.0/src/portals/resources/__init__.py +0 -0
- portals_api-0.1.0/src/portals/resources/_helpers.py +105 -0
- portals_api-0.1.0/src/portals/resources/arena.py +145 -0
- portals_api-0.1.0/src/portals/resources/collection_offers.py +210 -0
- portals_api-0.1.0/src/portals/resources/collections.py +151 -0
- portals_api-0.1.0/src/portals/resources/market.py +127 -0
- portals_api-0.1.0/src/portals/resources/nfts.py +446 -0
- portals_api-0.1.0/src/portals/resources/users.py +117 -0
- portals_api-0.1.0/src/portals/resources/wallet.py +88 -0
- portals_api-0.1.0/src/portals_api.egg-info/PKG-INFO +218 -0
- portals_api-0.1.0/src/portals_api.egg-info/SOURCES.txt +31 -0
- portals_api-0.1.0/src/portals_api.egg-info/dependency_links.txt +1 -0
- portals_api-0.1.0/src/portals_api.egg-info/requires.txt +2 -0
- portals_api-0.1.0/src/portals_api.egg-info/top_level.txt +1 -0
- portals_api-0.1.0/tests/test_contract.py +26 -0
- portals_api-0.1.0/tests/test_errors.py +110 -0
- portals_api-0.1.0/tests/test_models.py +90 -0
- portals_api-0.1.0/tests/test_resources.py +502 -0
- portals_api-0.1.0/tests/test_transport.py +152 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Portals API client contributors
|
|
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.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: portals-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A typed Python client for the Portals Partners API
|
|
5
|
+
Author: Portals API client contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NicolasKingreen/portals-python
|
|
8
|
+
Project-URL: Source, https://github.com/NicolasKingreen/portals-python
|
|
9
|
+
Project-URL: Issues, https://github.com/NicolasKingreen/portals-python/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/NicolasKingreen/portals-python/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: API documentation, https://docs.portals.tg/developers/api/
|
|
12
|
+
Keywords: portals,telegram,ton,nft,api,asyncio
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: httpx2<3,>=2.16
|
|
28
|
+
Requires-Dist: pydantic<3,>=2.8
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# Portals API Python client
|
|
32
|
+
|
|
33
|
+
A typed synchronous and asynchronous Python client for the [Portals Partners API](https://docs.portals.tg/developers/api/), the partner-facing programmatic API of the Portals marketplace.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- All 35 operations from the Portals Partners OpenAPI contract
|
|
38
|
+
- Matching `PortalsClient` and `AsyncPortalsClient` APIs
|
|
39
|
+
- Typed Pydantic v2 models for every schema in the contract (strict requests, forward-compatible responses)
|
|
40
|
+
- httpx2 connection pooling and injectable HTTP clients
|
|
41
|
+
- Exact `Decimal` amounts and parsed `datetime` timestamps
|
|
42
|
+
- Typed API errors and rate-limit metadata
|
|
43
|
+
- Python 3.11+ and a `py.typed` marker
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install portals-api
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv add portals-api
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
From a local checkout:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The distribution is named `portals-api`; the import package is `portals`.
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
### Synchronous
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from portals import PortalsClient
|
|
71
|
+
|
|
72
|
+
with PortalsClient("your-api-key") as client:
|
|
73
|
+
config = client.market.config()
|
|
74
|
+
wallet = client.wallet.get()
|
|
75
|
+
results = client.nfts.search(max_price="20", limit=20)
|
|
76
|
+
|
|
77
|
+
print(config.deposit_wallet)
|
|
78
|
+
print(wallet.balance)
|
|
79
|
+
print(results.total_count)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Asynchronous
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import asyncio
|
|
86
|
+
|
|
87
|
+
from portals import AsyncPortalsClient
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def main() -> None:
|
|
91
|
+
async with AsyncPortalsClient("your-api-key") as client:
|
|
92
|
+
wallet = await client.wallet.get()
|
|
93
|
+
state = await client.arena.state()
|
|
94
|
+
|
|
95
|
+
print(wallet.balance)
|
|
96
|
+
print(state.server_time)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
asyncio.run(main())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Get an API key from the Portals mini app under **Profile → Partner API**. Keep the key on your server and never commit it to source control.
|
|
103
|
+
|
|
104
|
+
## Resources
|
|
105
|
+
|
|
106
|
+
| Resource | Operations |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `client.arena` | Arena state, inventory, and bets |
|
|
109
|
+
| `client.offers` | Create, update, cancel, inspect, and accept collection offers |
|
|
110
|
+
| `client.collections` | Collection previews, metrics, and attribute floor prices |
|
|
111
|
+
| `client.market` | Market configuration and activity |
|
|
112
|
+
| `client.nfts` | Search, owned NFTs, purchases, listings, transfers, and withdrawals |
|
|
113
|
+
| `client.users` | User existence and authenticated user activity |
|
|
114
|
+
| `client.wallet` | Balance, deposits, TON withdrawals, and withdrawal statuses |
|
|
115
|
+
|
|
116
|
+
Methods return typed models. For example:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from decimal import Decimal
|
|
120
|
+
|
|
121
|
+
from portals import BuyDetail, PortalsClient
|
|
122
|
+
|
|
123
|
+
with PortalsClient("your-api-key") as client:
|
|
124
|
+
purchase = client.nfts.buy(
|
|
125
|
+
[
|
|
126
|
+
BuyDetail(id="nft-id", price=Decimal("12.50")),
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
print(purchase.total_purchased)
|
|
131
|
+
print(purchase.total_failed)
|
|
132
|
+
print(purchase.total_spent)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Purchases may partially succeed. Inspect each item in `purchase.purchase_results` rather than assuming the whole request succeeded or failed.
|
|
136
|
+
|
|
137
|
+
## Amounts and timestamps
|
|
138
|
+
|
|
139
|
+
Pass amounts as decimal strings or `Decimal` instances. Floats are rejected to avoid binary floating-point errors. Amounts are serialized as strings on the wire and exposed as `Decimal` values by response models.
|
|
140
|
+
|
|
141
|
+
RFC 3339 timestamps are exposed as `datetime` instances. Arena timestamps remain integers because the API returns Unix milliseconds.
|
|
142
|
+
|
|
143
|
+
## Errors
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from portals import APIError, PortalsClient, RateLimitError
|
|
147
|
+
|
|
148
|
+
with PortalsClient("your-api-key") as client:
|
|
149
|
+
try:
|
|
150
|
+
client.market.actions()
|
|
151
|
+
except RateLimitError as exc:
|
|
152
|
+
if exc.rate_limit is not None:
|
|
153
|
+
print(exc.rate_limit.reset_seconds)
|
|
154
|
+
except APIError as exc:
|
|
155
|
+
print(exc.status_code, exc.code, exc.message)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Specific exceptions cover authentication, access denial, rate limits, an unavailable market feed, and unknown Arena bet outcomes. Transport failures, invalid JSON, and response-validation failures have separate exception types.
|
|
159
|
+
|
|
160
|
+
Write operations are not automatically retried. In particular, an Arena bet that returns `504 BET_OUTCOME_UNKNOWN` or fails during network transmission may already have been placed; check `client.arena.state()` before trying again.
|
|
161
|
+
|
|
162
|
+
## Custom HTTP clients
|
|
163
|
+
|
|
164
|
+
Pass an `httpx2.Client` or `httpx2.AsyncClient` to customize transports, proxies, or test behavior. The library closes only clients it creates itself.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import httpx2
|
|
168
|
+
|
|
169
|
+
from portals import PortalsClient
|
|
170
|
+
|
|
171
|
+
http_client = httpx2.Client(timeout=10)
|
|
172
|
+
client = PortalsClient("your-api-key", client=http_client)
|
|
173
|
+
|
|
174
|
+
client.close()
|
|
175
|
+
http_client.close()
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Low-level requests
|
|
179
|
+
|
|
180
|
+
Use `request()` for new endpoints that do not yet have a typed method:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
payload = client.request("GET", "/partners/example", params={"limit": 10})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Pass `response_model=YourModel` to validate the response with Pydantic.
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
Install the project and development tools with [uv](https://docs.astral.sh/uv/):
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
uv sync
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Run the checks used by CI:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
uv run ruff check .
|
|
200
|
+
uv run ruff format --check .
|
|
201
|
+
uv run mypy
|
|
202
|
+
uv run pytest
|
|
203
|
+
uv build
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The OpenAPI contract is vendored at `specs/partners.en.json`. Refresh it and regenerate the
|
|
207
|
+
models, then review the resulting changes:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
uv run python scripts/update_spec.py
|
|
211
|
+
uv run python scripts/generate_models.py
|
|
212
|
+
uv run ruff check --fix src/portals/models.py
|
|
213
|
+
uv run ruff format src/portals/models.py
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Portals API Python client
|
|
2
|
+
|
|
3
|
+
A typed synchronous and asynchronous Python client for the [Portals Partners API](https://docs.portals.tg/developers/api/), the partner-facing programmatic API of the Portals marketplace.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- All 35 operations from the Portals Partners OpenAPI contract
|
|
8
|
+
- Matching `PortalsClient` and `AsyncPortalsClient` APIs
|
|
9
|
+
- Typed Pydantic v2 models for every schema in the contract (strict requests, forward-compatible responses)
|
|
10
|
+
- httpx2 connection pooling and injectable HTTP clients
|
|
11
|
+
- Exact `Decimal` amounts and parsed `datetime` timestamps
|
|
12
|
+
- Typed API errors and rate-limit metadata
|
|
13
|
+
- Python 3.11+ and a `py.typed` marker
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install portals-api
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv add portals-api
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
From a local checkout:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The distribution is named `portals-api`; the import package is `portals`.
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
### Synchronous
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from portals import PortalsClient
|
|
41
|
+
|
|
42
|
+
with PortalsClient("your-api-key") as client:
|
|
43
|
+
config = client.market.config()
|
|
44
|
+
wallet = client.wallet.get()
|
|
45
|
+
results = client.nfts.search(max_price="20", limit=20)
|
|
46
|
+
|
|
47
|
+
print(config.deposit_wallet)
|
|
48
|
+
print(wallet.balance)
|
|
49
|
+
print(results.total_count)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Asynchronous
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import asyncio
|
|
56
|
+
|
|
57
|
+
from portals import AsyncPortalsClient
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def main() -> None:
|
|
61
|
+
async with AsyncPortalsClient("your-api-key") as client:
|
|
62
|
+
wallet = await client.wallet.get()
|
|
63
|
+
state = await client.arena.state()
|
|
64
|
+
|
|
65
|
+
print(wallet.balance)
|
|
66
|
+
print(state.server_time)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Get an API key from the Portals mini app under **Profile → Partner API**. Keep the key on your server and never commit it to source control.
|
|
73
|
+
|
|
74
|
+
## Resources
|
|
75
|
+
|
|
76
|
+
| Resource | Operations |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `client.arena` | Arena state, inventory, and bets |
|
|
79
|
+
| `client.offers` | Create, update, cancel, inspect, and accept collection offers |
|
|
80
|
+
| `client.collections` | Collection previews, metrics, and attribute floor prices |
|
|
81
|
+
| `client.market` | Market configuration and activity |
|
|
82
|
+
| `client.nfts` | Search, owned NFTs, purchases, listings, transfers, and withdrawals |
|
|
83
|
+
| `client.users` | User existence and authenticated user activity |
|
|
84
|
+
| `client.wallet` | Balance, deposits, TON withdrawals, and withdrawal statuses |
|
|
85
|
+
|
|
86
|
+
Methods return typed models. For example:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from decimal import Decimal
|
|
90
|
+
|
|
91
|
+
from portals import BuyDetail, PortalsClient
|
|
92
|
+
|
|
93
|
+
with PortalsClient("your-api-key") as client:
|
|
94
|
+
purchase = client.nfts.buy(
|
|
95
|
+
[
|
|
96
|
+
BuyDetail(id="nft-id", price=Decimal("12.50")),
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
print(purchase.total_purchased)
|
|
101
|
+
print(purchase.total_failed)
|
|
102
|
+
print(purchase.total_spent)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Purchases may partially succeed. Inspect each item in `purchase.purchase_results` rather than assuming the whole request succeeded or failed.
|
|
106
|
+
|
|
107
|
+
## Amounts and timestamps
|
|
108
|
+
|
|
109
|
+
Pass amounts as decimal strings or `Decimal` instances. Floats are rejected to avoid binary floating-point errors. Amounts are serialized as strings on the wire and exposed as `Decimal` values by response models.
|
|
110
|
+
|
|
111
|
+
RFC 3339 timestamps are exposed as `datetime` instances. Arena timestamps remain integers because the API returns Unix milliseconds.
|
|
112
|
+
|
|
113
|
+
## Errors
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from portals import APIError, PortalsClient, RateLimitError
|
|
117
|
+
|
|
118
|
+
with PortalsClient("your-api-key") as client:
|
|
119
|
+
try:
|
|
120
|
+
client.market.actions()
|
|
121
|
+
except RateLimitError as exc:
|
|
122
|
+
if exc.rate_limit is not None:
|
|
123
|
+
print(exc.rate_limit.reset_seconds)
|
|
124
|
+
except APIError as exc:
|
|
125
|
+
print(exc.status_code, exc.code, exc.message)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Specific exceptions cover authentication, access denial, rate limits, an unavailable market feed, and unknown Arena bet outcomes. Transport failures, invalid JSON, and response-validation failures have separate exception types.
|
|
129
|
+
|
|
130
|
+
Write operations are not automatically retried. In particular, an Arena bet that returns `504 BET_OUTCOME_UNKNOWN` or fails during network transmission may already have been placed; check `client.arena.state()` before trying again.
|
|
131
|
+
|
|
132
|
+
## Custom HTTP clients
|
|
133
|
+
|
|
134
|
+
Pass an `httpx2.Client` or `httpx2.AsyncClient` to customize transports, proxies, or test behavior. The library closes only clients it creates itself.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
import httpx2
|
|
138
|
+
|
|
139
|
+
from portals import PortalsClient
|
|
140
|
+
|
|
141
|
+
http_client = httpx2.Client(timeout=10)
|
|
142
|
+
client = PortalsClient("your-api-key", client=http_client)
|
|
143
|
+
|
|
144
|
+
client.close()
|
|
145
|
+
http_client.close()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Low-level requests
|
|
149
|
+
|
|
150
|
+
Use `request()` for new endpoints that do not yet have a typed method:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
payload = client.request("GET", "/partners/example", params={"limit": 10})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Pass `response_model=YourModel` to validate the response with Pydantic.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
Install the project and development tools with [uv](https://docs.astral.sh/uv/):
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
uv sync
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Run the checks used by CI:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
uv run ruff check .
|
|
170
|
+
uv run ruff format --check .
|
|
171
|
+
uv run mypy
|
|
172
|
+
uv run pytest
|
|
173
|
+
uv build
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The OpenAPI contract is vendored at `specs/partners.en.json`. Refresh it and regenerate the
|
|
177
|
+
models, then review the resulting changes:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
uv run python scripts/update_spec.py
|
|
181
|
+
uv run python scripts/generate_models.py
|
|
182
|
+
uv run ruff check --fix src/portals/models.py
|
|
183
|
+
uv run ruff format src/portals/models.py
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "portals-api"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A typed Python client for the Portals Partners API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Portals API client contributors" }]
|
|
13
|
+
keywords = ["portals", "telegram", "ton", "nft", "api", "asyncio"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"httpx2>=2.16,<3",
|
|
16
|
+
"pydantic>=2.8,<3",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Framework :: AsyncIO",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Programming Language :: Python :: 3.14",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/NicolasKingreen/portals-python"
|
|
34
|
+
Source = "https://github.com/NicolasKingreen/portals-python"
|
|
35
|
+
Issues = "https://github.com/NicolasKingreen/portals-python/issues"
|
|
36
|
+
Changelog = "https://github.com/NicolasKingreen/portals-python/blob/main/CHANGELOG.md"
|
|
37
|
+
"API documentation" = "https://docs.portals.tg/developers/api/"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"mypy>=1.8",
|
|
42
|
+
"pytest>=8.0",
|
|
43
|
+
"pytest-asyncio>=0.24",
|
|
44
|
+
"pytest-cov>=5.0",
|
|
45
|
+
"ruff>=0.8",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools]
|
|
49
|
+
package-dir = { "" = "src" }
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.package-data]
|
|
55
|
+
portals = ["py.typed"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
target-version = "py311"
|
|
59
|
+
line-length = 100
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I", "UP", "B", "RUF"]
|
|
63
|
+
ignore = ["E501"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.per-file-ignores]
|
|
66
|
+
"src/portals/models.py" = ["RUF022"]
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.11"
|
|
70
|
+
strict = true
|
|
71
|
+
plugins = ["pydantic.mypy"]
|
|
72
|
+
files = ["src/portals", "tests"]
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|
|
76
|
+
pythonpath = ["."]
|
|
77
|
+
asyncio_mode = "auto"
|
|
78
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
79
|
+
addopts = "-ra --strict-markers"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Typed synchronous and asynchronous Python client for the Portals Partners API."""
|
|
2
|
+
|
|
3
|
+
from . import models as _models
|
|
4
|
+
from ._contract import __api_version__
|
|
5
|
+
from ._models import Amount, RateLimit
|
|
6
|
+
from ._version import __version__
|
|
7
|
+
from .client import AsyncPortalsClient, PortalsClient
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
AccessDeniedError,
|
|
10
|
+
APIError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
BetOutcomeUnknownError,
|
|
13
|
+
InvalidResponseError,
|
|
14
|
+
MarketFeedUnavailableError,
|
|
15
|
+
PortalsError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
ResponseValidationError,
|
|
18
|
+
TransportError,
|
|
19
|
+
)
|
|
20
|
+
from .models import * # noqa: F403
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AccessDeniedError",
|
|
24
|
+
"APIError",
|
|
25
|
+
"Amount",
|
|
26
|
+
"AsyncPortalsClient",
|
|
27
|
+
"AuthenticationError",
|
|
28
|
+
"BetOutcomeUnknownError",
|
|
29
|
+
"InvalidResponseError",
|
|
30
|
+
"MarketFeedUnavailableError",
|
|
31
|
+
"PortalsClient",
|
|
32
|
+
"PortalsError",
|
|
33
|
+
"RateLimit",
|
|
34
|
+
"RateLimitError",
|
|
35
|
+
"ResponseValidationError",
|
|
36
|
+
"TransportError",
|
|
37
|
+
"__api_version__",
|
|
38
|
+
"__version__",
|
|
39
|
+
*_models.__all__,
|
|
40
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from decimal import Decimal, InvalidOperation
|
|
6
|
+
from typing import Annotated, Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, BeforeValidator, ConfigDict, PlainSerializer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _amount(value: Any) -> Decimal:
|
|
12
|
+
if not isinstance(value, (str, Decimal)):
|
|
13
|
+
raise ValueError("Amounts must be decimal strings or Decimal instances")
|
|
14
|
+
try:
|
|
15
|
+
result = Decimal(value)
|
|
16
|
+
except InvalidOperation as exc:
|
|
17
|
+
raise ValueError(f"Invalid decimal amount: {value!r}") from exc
|
|
18
|
+
if not result.is_finite():
|
|
19
|
+
raise ValueError("Amounts must be finite")
|
|
20
|
+
return result
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _serialize_amount(value: Decimal) -> str:
|
|
24
|
+
return format(value, "f")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Amount = Annotated[
|
|
28
|
+
Decimal,
|
|
29
|
+
BeforeValidator(_amount),
|
|
30
|
+
PlainSerializer(_serialize_amount, return_type=str),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PortalsModel(BaseModel):
|
|
35
|
+
"""Common Pydantic base model for Portals API schemas."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PortalsRequest(PortalsModel):
|
|
41
|
+
"""Base model for API request payloads (`extra="forbid"`)."""
|
|
42
|
+
|
|
43
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PortalsResponse(PortalsModel):
|
|
47
|
+
"""Base model for API response payloads (`extra="allow"`)."""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class RateLimit:
|
|
54
|
+
"""Rate-limit state parsed from `X-RateLimit-*` response headers."""
|
|
55
|
+
|
|
56
|
+
limit: int
|
|
57
|
+
remaining: int
|
|
58
|
+
reset_seconds: int
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_headers(cls, headers: Mapping[str, str]) -> RateLimit | None:
|
|
62
|
+
"""Parse rate-limit headers from an HTTP response, or return `None` if absent."""
|
|
63
|
+
normalized = {key.lower(): value for key, value in headers.items()}
|
|
64
|
+
try:
|
|
65
|
+
return cls(
|
|
66
|
+
limit=int(normalized["x-ratelimit-limit"]),
|
|
67
|
+
remaining=int(normalized["x-ratelimit-remaining"]),
|
|
68
|
+
reset_seconds=int(normalized["x-ratelimit-reset"]),
|
|
69
|
+
)
|
|
70
|
+
except (KeyError, TypeError, ValueError):
|
|
71
|
+
return None
|