0xinsider 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.
- 0xinsider-0.1.0/.github/workflows/publish.yml +48 -0
- 0xinsider-0.1.0/.github/workflows/regenerate.yml +32 -0
- 0xinsider-0.1.0/.gitignore +4 -0
- 0xinsider-0.1.0/LICENSE +21 -0
- 0xinsider-0.1.0/PKG-INFO +87 -0
- 0xinsider-0.1.0/README.md +65 -0
- 0xinsider-0.1.0/examples/discovery.py +9 -0
- 0xinsider-0.1.0/examples/sandbox.py +15 -0
- 0xinsider-0.1.0/pyproject.toml +47 -0
- 0xinsider-0.1.0/scripts/generate.py +177 -0
- 0xinsider-0.1.0/src/oxinsider/__init__.py +41 -0
- 0xinsider-0.1.0/src/oxinsider/_client.py +208 -0
- 0xinsider-0.1.0/src/oxinsider/_errors.py +92 -0
- 0xinsider-0.1.0/src/oxinsider/_operations.py +1200 -0
- 0xinsider-0.1.0/src/oxinsider/_version.py +1 -0
- 0xinsider-0.1.0/src/oxinsider/py.typed +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes a GitHub release to https://pypi.org/project/0xinsider/ through
|
|
4
|
+
# PyPI trusted publishing: no API token is stored anywhere. The PyPI project
|
|
5
|
+
# lists this repository and this workflow file as its trusted publisher, and
|
|
6
|
+
# the `pypi` environment scopes the OIDC grant to this job.
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions: {}
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v6
|
|
22
|
+
- name: Check the tag matches the package version
|
|
23
|
+
if: github.event_name == 'release'
|
|
24
|
+
run: |
|
|
25
|
+
version=$(python3 -c "import re;print(re.search(r'\"(.+)\"', open('src/oxinsider/_version.py').read()).group(1))")
|
|
26
|
+
test "v${version}" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} does not match version ${version}"; exit 1; }
|
|
27
|
+
- run: uv build
|
|
28
|
+
- uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: dist
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
publish:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment: pypi
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/download-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
45
|
+
with:
|
|
46
|
+
# A version already on PyPI is skipped, not failed: a release event
|
|
47
|
+
# after a manual dispatch of the same version is then a no-op.
|
|
48
|
+
skip-existing: true
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Regenerate from OpenAPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "23 6 * * 1"
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
regenerate:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- run: python scripts/generate.py
|
|
21
|
+
- name: Open a pull request when the contract changed
|
|
22
|
+
env:
|
|
23
|
+
GH_TOKEN: ${{ github.token }}
|
|
24
|
+
run: |
|
|
25
|
+
if git diff --quiet; then echo "no change"; exit 0; fi
|
|
26
|
+
branch="regenerate/$(date -u +%Y%m%d)"
|
|
27
|
+
git config user.name "github-actions[bot]"
|
|
28
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
29
|
+
git checkout -b "$branch"
|
|
30
|
+
git commit -am "chore: regenerate operations from the OpenAPI document"
|
|
31
|
+
git push origin "$branch"
|
|
32
|
+
gh pr create --title "Regenerate operations from the OpenAPI document" --body "The published OpenAPI document changed. Review the generated diff before releasing."
|
0xinsider-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 0xinsider
|
|
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.
|
0xinsider-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: 0xinsider
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the 0xinsider Developer API: Polymarket sports and esports analytics, trader grades, large trades and sharp-money flow.
|
|
5
|
+
Project-URL: Homepage, https://0xinsider.com/developers
|
|
6
|
+
Project-URL: Documentation, https://docs.0xinsider.com
|
|
7
|
+
Project-URL: Repository, https://github.com/0xinsider/0xinsider-python
|
|
8
|
+
Project-URL: Issues, https://github.com/0xinsider/0xinsider-python/issues
|
|
9
|
+
Project-URL: OpenAPI, https://0xinsider.com/api/v1/openapi.json
|
|
10
|
+
Author-email: 0xinsider <support@0xinsider.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: 0xinsider,api,polymarket,prediction markets,sdk,sports analytics
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: httpx<1,>=0.27
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# 0xinsider Python SDK
|
|
24
|
+
|
|
25
|
+
Official Python client for the [0xinsider Developer API](https://0xinsider.com/developers): Polymarket sports and esports analytics, wallet grades, large trades, positions, reports and sharp-money flow.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pip install 0xinsider
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The distribution is `0xinsider`; the import is `oxinsider`, because a Python module name cannot start with a digit.
|
|
32
|
+
|
|
33
|
+
## Try it without a key
|
|
34
|
+
|
|
35
|
+
The [sandbox](https://0xinsider.com/sandbox/api/v1) answers every documented operation with example data. It needs no credential and never touches production data.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import oxinsider
|
|
39
|
+
|
|
40
|
+
with oxinsider.Client.sandbox() as client:
|
|
41
|
+
page = client.list_leaderboard(limit=5)
|
|
42
|
+
print(page["data"][0]["username"])
|
|
43
|
+
|
|
44
|
+
# Any error the operation documents, on demand:
|
|
45
|
+
try:
|
|
46
|
+
client.request("GET", "/api/v1/leaderboard", query={"sandbox_status": 429})
|
|
47
|
+
except oxinsider.RateLimitedError as error:
|
|
48
|
+
print(error.code, error.retry_after)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Live data
|
|
52
|
+
|
|
53
|
+
Data operations need an API key from [0xinsider.com/developers](https://0xinsider.com/developers) or an OAuth 2.1 access token ([auth.md](https://0xinsider.com/auth.md)), plus an active Pro subscription. Discovery, health and platforms are public.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import oxinsider
|
|
57
|
+
|
|
58
|
+
client = oxinsider.Client() # reads OXINSIDER_API_KEY
|
|
59
|
+
trader = client.get_trader("swisstony", expand=["strategy", "categories"])
|
|
60
|
+
print(trader["data"]["grade"])
|
|
61
|
+
|
|
62
|
+
for trade in client.paginate("list_whale_trades", min_grade="A", limit=100):
|
|
63
|
+
print(trade["size_usd"], trade["market"]["title"])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- Every documented operation is a method named after its operationId in snake_case (`listLeaderboard` becomes `list_leaderboard`). Each returns the decoded JSON body.
|
|
67
|
+
- `if_none_match="<etag>"` returns `{"object": "not_modified", "data": None, "etag": ...}` when nothing changed.
|
|
68
|
+
- `idempotency_key=` is accepted on the webhook mutations that document it.
|
|
69
|
+
- The SSE stream is read with `client.request("GET", "/api/v1/stream", stream=True)`, which returns the open `httpx.Response`.
|
|
70
|
+
|
|
71
|
+
## Errors
|
|
72
|
+
|
|
73
|
+
Every non-2xx response raises `oxinsider.OxinsiderApiError` or a subclass: `BadRequestError`, `AuthenticationError`, `SubscriptionRequiredError`, `PermissionDeniedError`, `NotFoundError`, `RateLimitedError` or `ServerError`. Each carries `status`, `code`, `reason`, `param`, `retry_at`, `retry_after` and `request_id`. Branch on `reason` when it is present. For `RateLimitedError`, wait `retry_after` seconds. A request that gets no response raises `OxinsiderConnectionError`.
|
|
74
|
+
|
|
75
|
+
## How it is built
|
|
76
|
+
|
|
77
|
+
`src/oxinsider/_operations.py` is generated from the published [OpenAPI document](https://0xinsider.com/api/v1/openapi.json) by `scripts/generate.py`, and a weekly workflow opens a pull request when the contract changes. Releases publish to PyPI from GitHub Actions through trusted publishing.
|
|
78
|
+
|
|
79
|
+
## Other official tools
|
|
80
|
+
|
|
81
|
+
- CLI and MCP server: `npm install --global @0xinsider/mcp` or `brew install 0xinsider/tap/oxinsider`
|
|
82
|
+
- Go SDK: `go get github.com/0xinsider/0xinsider-go`
|
|
83
|
+
- Remote MCP server: `https://api.0xinsider.com/api/v1/mcp`
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 0xinsider Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for the [0xinsider Developer API](https://0xinsider.com/developers): Polymarket sports and esports analytics, wallet grades, large trades, positions, reports and sharp-money flow.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install 0xinsider
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The distribution is `0xinsider`; the import is `oxinsider`, because a Python module name cannot start with a digit.
|
|
10
|
+
|
|
11
|
+
## Try it without a key
|
|
12
|
+
|
|
13
|
+
The [sandbox](https://0xinsider.com/sandbox/api/v1) answers every documented operation with example data. It needs no credential and never touches production data.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import oxinsider
|
|
17
|
+
|
|
18
|
+
with oxinsider.Client.sandbox() as client:
|
|
19
|
+
page = client.list_leaderboard(limit=5)
|
|
20
|
+
print(page["data"][0]["username"])
|
|
21
|
+
|
|
22
|
+
# Any error the operation documents, on demand:
|
|
23
|
+
try:
|
|
24
|
+
client.request("GET", "/api/v1/leaderboard", query={"sandbox_status": 429})
|
|
25
|
+
except oxinsider.RateLimitedError as error:
|
|
26
|
+
print(error.code, error.retry_after)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Live data
|
|
30
|
+
|
|
31
|
+
Data operations need an API key from [0xinsider.com/developers](https://0xinsider.com/developers) or an OAuth 2.1 access token ([auth.md](https://0xinsider.com/auth.md)), plus an active Pro subscription. Discovery, health and platforms are public.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import oxinsider
|
|
35
|
+
|
|
36
|
+
client = oxinsider.Client() # reads OXINSIDER_API_KEY
|
|
37
|
+
trader = client.get_trader("swisstony", expand=["strategy", "categories"])
|
|
38
|
+
print(trader["data"]["grade"])
|
|
39
|
+
|
|
40
|
+
for trade in client.paginate("list_whale_trades", min_grade="A", limit=100):
|
|
41
|
+
print(trade["size_usd"], trade["market"]["title"])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- Every documented operation is a method named after its operationId in snake_case (`listLeaderboard` becomes `list_leaderboard`). Each returns the decoded JSON body.
|
|
45
|
+
- `if_none_match="<etag>"` returns `{"object": "not_modified", "data": None, "etag": ...}` when nothing changed.
|
|
46
|
+
- `idempotency_key=` is accepted on the webhook mutations that document it.
|
|
47
|
+
- The SSE stream is read with `client.request("GET", "/api/v1/stream", stream=True)`, which returns the open `httpx.Response`.
|
|
48
|
+
|
|
49
|
+
## Errors
|
|
50
|
+
|
|
51
|
+
Every non-2xx response raises `oxinsider.OxinsiderApiError` or a subclass: `BadRequestError`, `AuthenticationError`, `SubscriptionRequiredError`, `PermissionDeniedError`, `NotFoundError`, `RateLimitedError` or `ServerError`. Each carries `status`, `code`, `reason`, `param`, `retry_at`, `retry_after` and `request_id`. Branch on `reason` when it is present. For `RateLimitedError`, wait `retry_after` seconds. A request that gets no response raises `OxinsiderConnectionError`.
|
|
52
|
+
|
|
53
|
+
## How it is built
|
|
54
|
+
|
|
55
|
+
`src/oxinsider/_operations.py` is generated from the published [OpenAPI document](https://0xinsider.com/api/v1/openapi.json) by `scripts/generate.py`, and a weekly workflow opens a pull request when the contract changes. Releases publish to PyPI from GitHub Actions through trusted publishing.
|
|
56
|
+
|
|
57
|
+
## Other official tools
|
|
58
|
+
|
|
59
|
+
- CLI and MCP server: `npm install --global @0xinsider/mcp` or `brew install 0xinsider/tap/oxinsider`
|
|
60
|
+
- Go SDK: `go get github.com/0xinsider/0xinsider-go`
|
|
61
|
+
- Remote MCP server: `https://api.0xinsider.com/api/v1/mcp`
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Read the public discovery document and health from the live API. No key needed."""
|
|
2
|
+
|
|
3
|
+
import oxinsider
|
|
4
|
+
|
|
5
|
+
with oxinsider.Client(api_key="") as client:
|
|
6
|
+
discovery = client.get_api_discovery()
|
|
7
|
+
print("api base:", discovery["data"]["api_base_url"])
|
|
8
|
+
health = client.get_health()
|
|
9
|
+
print("health:", health.get("data", {}).get("status", health))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Call the 0xinsider sandbox: no key, example data, simulated errors."""
|
|
2
|
+
|
|
3
|
+
import oxinsider
|
|
4
|
+
|
|
5
|
+
with oxinsider.Client.sandbox() as client:
|
|
6
|
+
leaderboard = client.list_leaderboard(limit=3)
|
|
7
|
+
print("leaderboard object:", leaderboard["object"], "rows:", len(leaderboard["data"]))
|
|
8
|
+
|
|
9
|
+
trader = client.get_trader("0x0000000000000000000000000000000000000001")
|
|
10
|
+
print("trader grade:", trader["data"]["grade"])
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
client.request("GET", "/api/v1/leaderboard", query={"sandbox_status": 429})
|
|
14
|
+
except oxinsider.RateLimitedError as error:
|
|
15
|
+
print("simulated rate limit:", error.code, "retry after", error.retry_after, "s")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "0xinsider"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Official Python client for the 0xinsider Developer API: Polymarket sports and esports analytics, trader grades, large trades and sharp-money flow."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "0xinsider", email = "support@0xinsider.com" }]
|
|
14
|
+
keywords = ["0xinsider", "polymarket", "prediction markets", "sports analytics", "api", "sdk"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://0xinsider.com/developers"
|
|
26
|
+
Documentation = "https://docs.0xinsider.com"
|
|
27
|
+
Repository = "https://github.com/0xinsider/0xinsider-python"
|
|
28
|
+
Issues = "https://github.com/0xinsider/0xinsider-python/issues"
|
|
29
|
+
"OpenAPI" = "https://0xinsider.com/api/v1/openapi.json"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.version]
|
|
32
|
+
path = "src/oxinsider/_version.py"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/oxinsider"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
target-version = "py39"
|
|
39
|
+
line-length = 120
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "W", "B", "UP"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint.per-file-ignores]
|
|
45
|
+
# Generated: docstrings quote the OpenAPI descriptions verbatim.
|
|
46
|
+
"src/oxinsider/_operations.py" = ["E501"]
|
|
47
|
+
"scripts/generate.py" = ["E501"]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Generate src/oxinsider/_operations.py from the published 0xinsider OpenAPI document.
|
|
2
|
+
|
|
3
|
+
Usage: python scripts/generate.py [path-or-url]
|
|
4
|
+
Default source: https://0xinsider.com/api/v1/openapi.json
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import keyword
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import textwrap
|
|
14
|
+
import urllib.request
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
DEFAULT_SOURCE = "https://0xinsider.com/api/v1/openapi.json"
|
|
18
|
+
OUTPUT = Path(__file__).resolve().parent.parent / "src" / "oxinsider" / "_operations.py"
|
|
19
|
+
METHODS = ("get", "post", "put", "patch", "delete")
|
|
20
|
+
# Operations whose success response is a Server-Sent Events stream. They are
|
|
21
|
+
# reachable through Client.request(..., stream=True), not a generated method.
|
|
22
|
+
STREAMING_CONTENT = "text/event-stream"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load(source: str) -> dict:
|
|
26
|
+
if source.startswith("http://") or source.startswith("https://"):
|
|
27
|
+
request = urllib.request.Request(source, headers={"User-Agent": "0xinsider-python-generator"})
|
|
28
|
+
with urllib.request.urlopen(request, timeout=60) as response: # noqa: S310 - fixed https source
|
|
29
|
+
return json.load(response)
|
|
30
|
+
return json.loads(Path(source).read_text())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def snake(name: str) -> str:
|
|
34
|
+
name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
|
|
35
|
+
name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
|
|
36
|
+
return name.lower()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def python_name(name: str) -> str:
|
|
40
|
+
candidate = re.sub(r"[^0-9a-zA-Z_]", "_", name.replace("[]", ""))
|
|
41
|
+
if keyword.iskeyword(candidate) or candidate in {"self", "body", "headers"}:
|
|
42
|
+
candidate += "_"
|
|
43
|
+
return candidate
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def first_sentence(text: str, limit: int = 300) -> str:
|
|
47
|
+
text = " ".join((text or "").split())
|
|
48
|
+
if len(text) <= limit:
|
|
49
|
+
return text
|
|
50
|
+
cut = text[:limit]
|
|
51
|
+
return cut[: cut.rfind(" ")] + " ..."
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def success_content_types(operation: dict) -> list[str]:
|
|
55
|
+
for code in ("200", "201", "202"):
|
|
56
|
+
response = operation.get("responses", {}).get(code)
|
|
57
|
+
if isinstance(response, dict):
|
|
58
|
+
return list((response.get("content") or {}).keys())
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def generate(doc: dict) -> str:
|
|
63
|
+
entries = []
|
|
64
|
+
methods = []
|
|
65
|
+
for path, item in doc.get("paths", {}).items():
|
|
66
|
+
shared_params = item.get("parameters", [])
|
|
67
|
+
for method in METHODS:
|
|
68
|
+
operation = item.get(method)
|
|
69
|
+
if not isinstance(operation, dict):
|
|
70
|
+
continue
|
|
71
|
+
operation_id = operation.get("operationId")
|
|
72
|
+
if not operation_id:
|
|
73
|
+
continue
|
|
74
|
+
params = [*shared_params, *operation.get("parameters", [])]
|
|
75
|
+
seen = set()
|
|
76
|
+
unique = []
|
|
77
|
+
for param in params:
|
|
78
|
+
if "$ref" in param or param.get("name", "").endswith("[]"):
|
|
79
|
+
continue
|
|
80
|
+
key = (param.get("in"), param.get("name"))
|
|
81
|
+
if key in seen:
|
|
82
|
+
continue
|
|
83
|
+
seen.add(key)
|
|
84
|
+
unique.append(param)
|
|
85
|
+
path_params = [p for p in unique if p.get("in") == "path"]
|
|
86
|
+
query_params = [p for p in unique if p.get("in") == "query"]
|
|
87
|
+
header_params = [p for p in unique if p.get("in") == "header"]
|
|
88
|
+
has_body = "requestBody" in operation
|
|
89
|
+
content_types = success_content_types(operation)
|
|
90
|
+
streaming = content_types == [STREAMING_CONTENT]
|
|
91
|
+
entries.append(
|
|
92
|
+
{
|
|
93
|
+
"operation_id": operation_id,
|
|
94
|
+
"method": method.upper(),
|
|
95
|
+
"path": path,
|
|
96
|
+
"streaming": streaming,
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
if streaming:
|
|
100
|
+
continue
|
|
101
|
+
name = snake(operation_id)
|
|
102
|
+
args = ["self"]
|
|
103
|
+
args += [f"{python_name(p['name'])}: str" for p in path_params]
|
|
104
|
+
if has_body:
|
|
105
|
+
required = operation["requestBody"].get("required", False)
|
|
106
|
+
args.append("body: Any" if required else "body: Any = None")
|
|
107
|
+
kw = [f"{python_name(p['name'])}: Any = None" for p in query_params]
|
|
108
|
+
for header in header_params:
|
|
109
|
+
hname = header["name"].lower()
|
|
110
|
+
if hname == "if-none-match":
|
|
111
|
+
kw.append("if_none_match: str | None = None")
|
|
112
|
+
elif hname == "idempotency-key":
|
|
113
|
+
kw.append("idempotency_key: str | None = None")
|
|
114
|
+
if kw:
|
|
115
|
+
args.append("*")
|
|
116
|
+
args += kw
|
|
117
|
+
summary = first_sentence(operation.get("summary") or operation_id, 200)
|
|
118
|
+
description = first_sentence(operation.get("description") or "")
|
|
119
|
+
doc_lines = [f"{summary}.".replace("..", "."), "", f"``{method.upper()} {path}`` (operationId ``{operation_id}``)."]
|
|
120
|
+
if description:
|
|
121
|
+
doc_lines += ["", *textwrap.wrap(description, 88)]
|
|
122
|
+
if query_params:
|
|
123
|
+
doc_lines += ["", "Query parameters:"]
|
|
124
|
+
for p in query_params:
|
|
125
|
+
pdesc = first_sentence(p.get("description") or "", 160)
|
|
126
|
+
doc_lines.append(f" {python_name(p['name'])}: {pdesc}".rstrip())
|
|
127
|
+
docstring = "\n ".join(line.replace('"""', "'''") for line in doc_lines)
|
|
128
|
+
path_map = ", ".join(f'"{p["name"]}": {python_name(p["name"])}' for p in path_params)
|
|
129
|
+
query_map = ", ".join(f'"{p["name"]}": {python_name(p["name"])}' for p in query_params)
|
|
130
|
+
header_args = []
|
|
131
|
+
if any(h["name"].lower() == "if-none-match" for h in header_params):
|
|
132
|
+
header_args.append("if_none_match=if_none_match")
|
|
133
|
+
if any(h["name"].lower() == "idempotency-key" for h in header_params):
|
|
134
|
+
header_args.append("idempotency_key=idempotency_key")
|
|
135
|
+
call = [f'"{operation_id}"', f"path_params={{{path_map}}}", f"query={{{query_map}}}"]
|
|
136
|
+
if has_body:
|
|
137
|
+
call.append("body=body")
|
|
138
|
+
call += header_args
|
|
139
|
+
signature = ",\n ".join(args)
|
|
140
|
+
methods.append(
|
|
141
|
+
f" def {name}(\n {signature},\n ) -> Any:\n"
|
|
142
|
+
f' """{docstring}\n """\n'
|
|
143
|
+
f" return self._call({', '.join(call)})\n"
|
|
144
|
+
)
|
|
145
|
+
table = ",\n".join(
|
|
146
|
+
f' "{e["operation_id"]}": Operation("{e["method"]}", "{e["path"]}", streaming={e["streaming"]})'
|
|
147
|
+
for e in entries
|
|
148
|
+
)
|
|
149
|
+
version = doc.get("info", {}).get("version", "unknown")
|
|
150
|
+
header = (
|
|
151
|
+
'"""Generated from the 0xinsider OpenAPI document by scripts/generate.py. Do not edit."""\n\n'
|
|
152
|
+
"from __future__ import annotations\n\n"
|
|
153
|
+
"from typing import Any, NamedTuple\n\n\n"
|
|
154
|
+
f'OPENAPI_VERSION = "{version}"\n\n\n'
|
|
155
|
+
"class Operation(NamedTuple):\n"
|
|
156
|
+
" method: str\n"
|
|
157
|
+
" path: str\n"
|
|
158
|
+
" streaming: bool = False\n\n\n"
|
|
159
|
+
f"OPERATIONS: dict[str, Operation] = {{\n{table},\n}}\n\n\n"
|
|
160
|
+
"class OperationsMixin:\n"
|
|
161
|
+
' """One method per documented operation. Each returns the decoded JSON body."""\n\n'
|
|
162
|
+
" def _call(self, operation_id: str, **kwargs: Any) -> Any: # pragma: no cover - provided by Client\n"
|
|
163
|
+
" raise NotImplementedError\n\n"
|
|
164
|
+
)
|
|
165
|
+
source = header + "\n".join(methods)
|
|
166
|
+
# Blank docstring lines must not carry the indentation.
|
|
167
|
+
return re.sub(r"[ \t]+\n", "\n", source)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main() -> None:
|
|
171
|
+
source = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_SOURCE
|
|
172
|
+
OUTPUT.write_text(generate(load(source)))
|
|
173
|
+
print(f"wrote {OUTPUT}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Official Python client for the 0xinsider Developer API.
|
|
2
|
+
|
|
3
|
+
https://0xinsider.com/developers
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ._client import API_KEY_ENV, NOT_MODIFIED, PRODUCTION_BASE_URL, SANDBOX_BASE_URL, Client
|
|
7
|
+
from ._errors import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
BadRequestError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
OxinsiderApiError,
|
|
12
|
+
OxinsiderConnectionError,
|
|
13
|
+
OxinsiderError,
|
|
14
|
+
PermissionDeniedError,
|
|
15
|
+
RateLimitedError,
|
|
16
|
+
ServerError,
|
|
17
|
+
SubscriptionRequiredError,
|
|
18
|
+
)
|
|
19
|
+
from ._operations import OPENAPI_VERSION, OPERATIONS
|
|
20
|
+
from ._version import __version__
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"API_KEY_ENV",
|
|
24
|
+
"NOT_MODIFIED",
|
|
25
|
+
"OPENAPI_VERSION",
|
|
26
|
+
"OPERATIONS",
|
|
27
|
+
"PRODUCTION_BASE_URL",
|
|
28
|
+
"SANDBOX_BASE_URL",
|
|
29
|
+
"AuthenticationError",
|
|
30
|
+
"BadRequestError",
|
|
31
|
+
"Client",
|
|
32
|
+
"NotFoundError",
|
|
33
|
+
"OxinsiderApiError",
|
|
34
|
+
"OxinsiderConnectionError",
|
|
35
|
+
"OxinsiderError",
|
|
36
|
+
"PermissionDeniedError",
|
|
37
|
+
"RateLimitedError",
|
|
38
|
+
"ServerError",
|
|
39
|
+
"SubscriptionRequiredError",
|
|
40
|
+
"__version__",
|
|
41
|
+
]
|