gangtise-openapi 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.
- gangtise_openapi-0.1.0/.gitignore +16 -0
- gangtise_openapi-0.1.0/CHANGELOG.md +16 -0
- gangtise_openapi-0.1.0/LICENSE +21 -0
- gangtise_openapi-0.1.0/PKG-INFO +90 -0
- gangtise_openapi-0.1.0/README.md +64 -0
- gangtise_openapi-0.1.0/pyproject.toml +84 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/__about__.py +1 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/__init__.py +28 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_async_content.py +98 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_auth.py +89 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_client.py +456 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_config.py +56 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_download.py +256 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_endpoints.py +527 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_errors.py +58 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_facade.py +168 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_lookup/__init__.py +30 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_lookup/announcement_categories.py +97 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_lookup/broker_orgs.py +185 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_lookup/industries.py +36 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_lookup/industry_codes.py +36 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_lookup/meeting_orgs.py +135 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_lookup/regions.py +24 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_lookup/research_areas.py +71 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_lookup/theme_ids.py +408 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_normalize.py +25 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_pagination.py +168 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_quote_sharding.py +90 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_title_cache.py +106 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_transport.py +161 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/_transport_async.py +101 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/__init__.py +30 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/ai.py +570 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/alternative.py +141 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/auth.py +62 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/fundamental.py +729 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/insight.py +1386 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/lookup.py +98 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/quote.py +476 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/reference.py +99 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/domains/vault.py +520 -0
- gangtise_openapi-0.1.0/src/gangtise_openapi/py.typed +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-05-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Initial release.
|
|
11
|
+
- 73 endpoints from gangtise-openapi-cli v0.14.2.
|
|
12
|
+
- Sync (`gangtise`) and async (`gangtise.async_`) APIs.
|
|
13
|
+
- DataFrame-by-default returns with `raw=True` escape hatch.
|
|
14
|
+
- Auto-pagination concurrency, retry + token self-heal, K-line full-market date sharding, transparent async-content polling.
|
|
15
|
+
- Token cache shared with the npm CLI at `~/.config/gangtise/token.json`.
|
|
16
|
+
- Initial scaffold (pyproject, tooling, CI).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gangtiser
|
|
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,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gangtise-openapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Gangtise OpenAPI
|
|
5
|
+
Project-URL: Homepage, https://github.com/gangtiser/gangtise-python
|
|
6
|
+
Project-URL: Source, https://github.com/gangtiser/gangtise-python
|
|
7
|
+
Project-URL: Issues, https://github.com/gangtiser/gangtise-python/issues
|
|
8
|
+
Author: gangtiser
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: finance,gangtise,openapi,quote,research,sdk
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: anyio>=4.0
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: pandas>=2.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# gangtise-openapi
|
|
28
|
+
|
|
29
|
+
Python SDK for [Gangtise OpenAPI](https://open.gangtise.com). Feature-parity with the npm CLI [`gangtise-openapi-cli`](https://github.com/gangtiser/gangtise-openapi-cli) v0.14.2 across 73 endpoints.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install gangtise-openapi
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires Python 3.10+.
|
|
38
|
+
|
|
39
|
+
## Configure
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export GANGTISE_ACCESS_KEY=ak_xxx
|
|
43
|
+
export GANGTISE_SECRET_KEY=sk_xxx
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
(Or pass `access_key=` and `secret_key=` explicitly to `GangtiseClient`.) The token cache file at `~/.config/gangtise/token.json` is shared with the npm CLI.
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from gangtise_openapi import gangtise
|
|
52
|
+
|
|
53
|
+
# Tabular endpoints return a pandas DataFrame
|
|
54
|
+
df = gangtise.quote.day_kline(
|
|
55
|
+
security="000001.SH",
|
|
56
|
+
start_date="2026-01-01",
|
|
57
|
+
end_date="2026-01-31",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Use raw=True to get the underlying dict/list
|
|
61
|
+
result = gangtise.insight.opinion_list(industry=1, size=20, raw=True)
|
|
62
|
+
|
|
63
|
+
# Async
|
|
64
|
+
import asyncio
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
df = await gangtise.async_.quote.day_kline(security="000001.SH")
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Endpoints
|
|
73
|
+
|
|
74
|
+
The SDK exposes 73 endpoints across 9 domains:
|
|
75
|
+
|
|
76
|
+
- `gangtise.auth.*` — login, status
|
|
77
|
+
- `gangtise.lookup.*` — local lookup tables (research areas, brokers, industries, ...)
|
|
78
|
+
- `gangtise.reference.*` — securities search (GTS codes)
|
|
79
|
+
- `gangtise.insight.*` — opinions, research reports, announcements, schedules
|
|
80
|
+
- `gangtise.quote.*` — K-line, real-time quotes
|
|
81
|
+
- `gangtise.fundamental.*` — financial statements, valuation, holders, forecasts
|
|
82
|
+
- `gangtise.ai.*` — AI-generated insights (one-pager, peer comparison, earnings reviews, ...)
|
|
83
|
+
- `gangtise.vault.*` — personal drive, meeting records, stock pools, WeChat
|
|
84
|
+
- `gangtise.alternative.*` — economic indicators (EDB)
|
|
85
|
+
|
|
86
|
+
See the [npm CLI README](https://github.com/gangtiser/gangtise-openapi-cli#readme) for endpoint-by-endpoint documentation; the Python wrappers accept the same parameters as the CLI flags (snake_case instead of `--kebab-case`).
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# gangtise-openapi
|
|
2
|
+
|
|
3
|
+
Python SDK for [Gangtise OpenAPI](https://open.gangtise.com). Feature-parity with the npm CLI [`gangtise-openapi-cli`](https://github.com/gangtiser/gangtise-openapi-cli) v0.14.2 across 73 endpoints.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install gangtise-openapi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.10+.
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export GANGTISE_ACCESS_KEY=ak_xxx
|
|
17
|
+
export GANGTISE_SECRET_KEY=sk_xxx
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
(Or pass `access_key=` and `secret_key=` explicitly to `GangtiseClient`.) The token cache file at `~/.config/gangtise/token.json` is shared with the npm CLI.
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from gangtise_openapi import gangtise
|
|
26
|
+
|
|
27
|
+
# Tabular endpoints return a pandas DataFrame
|
|
28
|
+
df = gangtise.quote.day_kline(
|
|
29
|
+
security="000001.SH",
|
|
30
|
+
start_date="2026-01-01",
|
|
31
|
+
end_date="2026-01-31",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Use raw=True to get the underlying dict/list
|
|
35
|
+
result = gangtise.insight.opinion_list(industry=1, size=20, raw=True)
|
|
36
|
+
|
|
37
|
+
# Async
|
|
38
|
+
import asyncio
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
df = await gangtise.async_.quote.day_kline(security="000001.SH")
|
|
42
|
+
|
|
43
|
+
asyncio.run(main())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Endpoints
|
|
47
|
+
|
|
48
|
+
The SDK exposes 73 endpoints across 9 domains:
|
|
49
|
+
|
|
50
|
+
- `gangtise.auth.*` — login, status
|
|
51
|
+
- `gangtise.lookup.*` — local lookup tables (research areas, brokers, industries, ...)
|
|
52
|
+
- `gangtise.reference.*` — securities search (GTS codes)
|
|
53
|
+
- `gangtise.insight.*` — opinions, research reports, announcements, schedules
|
|
54
|
+
- `gangtise.quote.*` — K-line, real-time quotes
|
|
55
|
+
- `gangtise.fundamental.*` — financial statements, valuation, holders, forecasts
|
|
56
|
+
- `gangtise.ai.*` — AI-generated insights (one-pager, peer comparison, earnings reviews, ...)
|
|
57
|
+
- `gangtise.vault.*` — personal drive, meeting records, stock pools, WeChat
|
|
58
|
+
- `gangtise.alternative.*` — economic indicators (EDB)
|
|
59
|
+
|
|
60
|
+
See the [npm CLI README](https://github.com/gangtiser/gangtise-openapi-cli#readme) for endpoint-by-endpoint documentation; the Python wrappers accept the same parameters as the CLI flags (snake_case instead of `--kebab-case`).
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gangtise-openapi"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python SDK for Gangtise OpenAPI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "gangtiser" }]
|
|
13
|
+
keywords = ["gangtise", "finance", "openapi", "sdk", "quote", "research"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Office/Business :: Financial",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"httpx>=0.27",
|
|
27
|
+
"pandas>=2.0",
|
|
28
|
+
"anyio>=4.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/gangtiser/gangtise-python"
|
|
33
|
+
Source = "https://github.com/gangtiser/gangtise-python"
|
|
34
|
+
Issues = "https://github.com/gangtiser/gangtise-python/issues"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8.0",
|
|
39
|
+
"anyio[trio]>=4.0",
|
|
40
|
+
"respx>=0.21",
|
|
41
|
+
"ruff>=0.6",
|
|
42
|
+
"mypy>=1.10",
|
|
43
|
+
"pandas-stubs",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.version]
|
|
47
|
+
path = "src/gangtise_openapi/__about__.py"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/gangtise_openapi"]
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.sdist]
|
|
53
|
+
include = ["src/", "README.md", "LICENSE", "CHANGELOG.md", "pyproject.toml"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 100
|
|
57
|
+
target-version = "py310"
|
|
58
|
+
src = ["src", "tests"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
|
|
62
|
+
ignore = ["E501"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint.per-file-ignores]
|
|
65
|
+
"tests/**" = ["B008"]
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
files = ["src"]
|
|
69
|
+
python_version = "3.10"
|
|
70
|
+
strict = true
|
|
71
|
+
warn_unused_ignores = true
|
|
72
|
+
warn_return_any = true
|
|
73
|
+
ignore_missing_imports = false
|
|
74
|
+
|
|
75
|
+
[[tool.mypy.overrides]]
|
|
76
|
+
module = ["pandas.*", "respx.*"]
|
|
77
|
+
ignore_missing_imports = true
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
testpaths = ["tests"]
|
|
81
|
+
addopts = "-ra --strict-markers"
|
|
82
|
+
markers = [
|
|
83
|
+
"live: hits real Gangtise API (skipped by default; run with `pytest -m live`)",
|
|
84
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging as _logging
|
|
2
|
+
import os as _os
|
|
3
|
+
|
|
4
|
+
from gangtise_openapi.__about__ import __version__
|
|
5
|
+
from gangtise_openapi._client import AsyncGangtiseClient, GangtiseClient
|
|
6
|
+
from gangtise_openapi._errors import (
|
|
7
|
+
ApiError,
|
|
8
|
+
ConfigError,
|
|
9
|
+
DownloadError,
|
|
10
|
+
GangtiseError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
from gangtise_openapi._facade import gangtise
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ApiError",
|
|
17
|
+
"AsyncGangtiseClient",
|
|
18
|
+
"ConfigError",
|
|
19
|
+
"DownloadError",
|
|
20
|
+
"GangtiseClient",
|
|
21
|
+
"GangtiseError",
|
|
22
|
+
"ValidationError",
|
|
23
|
+
"__version__",
|
|
24
|
+
"gangtise",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
if _os.environ.get("GANGTISE_VERBOSE") in {"1", "true", "True", "yes", "YES"}:
|
|
28
|
+
_logging.getLogger("gangtise_openapi").setLevel(_logging.DEBUG)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import anyio
|
|
8
|
+
|
|
9
|
+
from gangtise_openapi._errors import ApiError
|
|
10
|
+
|
|
11
|
+
POLL_MAX_ATTEMPTS = 14
|
|
12
|
+
_INITIAL_DELAY_S = 5.0
|
|
13
|
+
_MAX_DELAY_S = 30.0
|
|
14
|
+
_GROWTH = 1.6
|
|
15
|
+
|
|
16
|
+
CODE_PENDING = "410110"
|
|
17
|
+
CODE_TERMINAL = "410111"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def next_delay_seconds(attempt: int) -> float:
|
|
21
|
+
grown = _INITIAL_DELAY_S * (_GROWTH ** (attempt - 1))
|
|
22
|
+
return min(_MAX_DELAY_S, float(round(grown)))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Fetcher = Callable[[], Any]
|
|
26
|
+
Sleeper = Callable[[float], None]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _classify(error: ApiError) -> str:
|
|
30
|
+
if error.code == CODE_PENDING:
|
|
31
|
+
return "pending"
|
|
32
|
+
if error.code == CODE_TERMINAL:
|
|
33
|
+
return "terminal"
|
|
34
|
+
return "other"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def poll_content(
|
|
38
|
+
fetch: Fetcher,
|
|
39
|
+
*,
|
|
40
|
+
sleep: Sleeper = time.sleep,
|
|
41
|
+
max_attempts: int = POLL_MAX_ATTEMPTS,
|
|
42
|
+
) -> Any:
|
|
43
|
+
last_pending: ApiError | None = None
|
|
44
|
+
for attempt in range(1, max_attempts + 1):
|
|
45
|
+
try:
|
|
46
|
+
result = fetch()
|
|
47
|
+
except ApiError as error:
|
|
48
|
+
kind = _classify(error)
|
|
49
|
+
if kind == "terminal":
|
|
50
|
+
raise ApiError(
|
|
51
|
+
"Content generation failed (terminal). Do not retry.",
|
|
52
|
+
code=CODE_TERMINAL,
|
|
53
|
+
) from error
|
|
54
|
+
if kind == "other":
|
|
55
|
+
raise
|
|
56
|
+
last_pending = error
|
|
57
|
+
else:
|
|
58
|
+
if isinstance(result, dict) and result.get("content") is not None:
|
|
59
|
+
return result
|
|
60
|
+
if attempt < max_attempts:
|
|
61
|
+
sleep(next_delay_seconds(attempt))
|
|
62
|
+
raise ApiError(
|
|
63
|
+
f"Content not available after {max_attempts} attempts",
|
|
64
|
+
code=CODE_PENDING,
|
|
65
|
+
) from last_pending
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
AsyncFetcher = Callable[[], Any]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def poll_content_async(
|
|
72
|
+
fetch: AsyncFetcher,
|
|
73
|
+
*,
|
|
74
|
+
max_attempts: int = POLL_MAX_ATTEMPTS,
|
|
75
|
+
) -> Any:
|
|
76
|
+
last_pending: ApiError | None = None
|
|
77
|
+
for attempt in range(1, max_attempts + 1):
|
|
78
|
+
try:
|
|
79
|
+
result = await fetch()
|
|
80
|
+
except ApiError as error:
|
|
81
|
+
kind = _classify(error)
|
|
82
|
+
if kind == "terminal":
|
|
83
|
+
raise ApiError(
|
|
84
|
+
"Content generation failed (terminal). Do not retry.",
|
|
85
|
+
code=CODE_TERMINAL,
|
|
86
|
+
) from error
|
|
87
|
+
if kind == "other":
|
|
88
|
+
raise
|
|
89
|
+
last_pending = error
|
|
90
|
+
else:
|
|
91
|
+
if isinstance(result, dict) and result.get("content") is not None:
|
|
92
|
+
return result
|
|
93
|
+
if attempt < max_attempts:
|
|
94
|
+
await anyio.sleep(next_delay_seconds(attempt))
|
|
95
|
+
raise ApiError(
|
|
96
|
+
f"Content not available after {max_attempts} attempts",
|
|
97
|
+
code=CODE_PENDING,
|
|
98
|
+
) from last_pending
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from gangtise_openapi._errors import ConfigError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class TokenCache:
|
|
14
|
+
access_token: str
|
|
15
|
+
expires_in: int
|
|
16
|
+
time: int
|
|
17
|
+
expires_at: int
|
|
18
|
+
uid: int | None = None
|
|
19
|
+
user_name: str | None = None
|
|
20
|
+
tenant_id: int | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_BUFFER_SECONDS = 300
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def normalize_token(token: str) -> str:
|
|
27
|
+
return token if token.startswith("Bearer ") else f"Bearer {token}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_cache_valid(cache: TokenCache | None, buffer_seconds: int = _BUFFER_SECONDS) -> bool:
|
|
31
|
+
if cache is None or not cache.access_token or not cache.expires_at:
|
|
32
|
+
return False
|
|
33
|
+
now = int(time.time())
|
|
34
|
+
return (cache.expires_at - buffer_seconds) > now
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_token_cache(path: Path) -> TokenCache | None:
|
|
38
|
+
try:
|
|
39
|
+
raw = path.read_text(encoding="utf8")
|
|
40
|
+
except OSError:
|
|
41
|
+
return None
|
|
42
|
+
try:
|
|
43
|
+
data: object = json.loads(raw)
|
|
44
|
+
except json.JSONDecodeError:
|
|
45
|
+
return None
|
|
46
|
+
if not isinstance(data, dict):
|
|
47
|
+
return None
|
|
48
|
+
access_token = data.get("accessToken")
|
|
49
|
+
expires_at = data.get("expiresAt")
|
|
50
|
+
if not isinstance(access_token, str) or not isinstance(expires_at, int):
|
|
51
|
+
return None
|
|
52
|
+
expires_in_raw = data.get("expiresIn", 0)
|
|
53
|
+
time_raw = data.get("time", 0)
|
|
54
|
+
uid = data.get("uid")
|
|
55
|
+
user_name = data.get("userName")
|
|
56
|
+
tenant_id = data.get("tenantId")
|
|
57
|
+
return TokenCache(
|
|
58
|
+
access_token=access_token,
|
|
59
|
+
expires_in=int(expires_in_raw) if isinstance(expires_in_raw, int) else 0,
|
|
60
|
+
time=int(time_raw) if isinstance(time_raw, int) else 0,
|
|
61
|
+
expires_at=expires_at,
|
|
62
|
+
uid=uid if isinstance(uid, int) else None,
|
|
63
|
+
user_name=user_name if isinstance(user_name, str) else None,
|
|
64
|
+
tenant_id=tenant_id if isinstance(tenant_id, int) else None,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def write_token_cache(path: Path, cache: TokenCache) -> None:
|
|
69
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
payload = {
|
|
71
|
+
"accessToken": cache.access_token,
|
|
72
|
+
"expiresIn": cache.expires_in,
|
|
73
|
+
"time": cache.time,
|
|
74
|
+
"expiresAt": cache.expires_at,
|
|
75
|
+
"uid": cache.uid,
|
|
76
|
+
"userName": cache.user_name,
|
|
77
|
+
"tenantId": cache.tenant_id,
|
|
78
|
+
}
|
|
79
|
+
# Atomic write: temp file + rename to avoid partial reads.
|
|
80
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
81
|
+
tmp.write_text(json.dumps(payload, indent=2), encoding="utf8")
|
|
82
|
+
os.chmod(tmp, 0o600)
|
|
83
|
+
tmp.replace(path)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def require_credentials(access_key: str | None, secret_key: str | None) -> tuple[str, str]:
|
|
87
|
+
if not access_key or not secret_key:
|
|
88
|
+
raise ConfigError("Missing GANGTISE_ACCESS_KEY or GANGTISE_SECRET_KEY")
|
|
89
|
+
return access_key, secret_key
|