twmarketdata 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.
- twmarketdata-0.1.0/.github/workflows/ci.yml +147 -0
- twmarketdata-0.1.0/.gitignore +12 -0
- twmarketdata-0.1.0/LICENSE +21 -0
- twmarketdata-0.1.0/PKG-INFO +319 -0
- twmarketdata-0.1.0/PYPI_PUBLISH_OWNER.md +113 -0
- twmarketdata-0.1.0/README.md +268 -0
- twmarketdata-0.1.0/pyproject.toml +53 -0
- twmarketdata-0.1.0/src/twmd/__init__.py +63 -0
- twmarketdata-0.1.0/src/twmd/access.py +145 -0
- twmarketdata-0.1.0/src/twmd/client.py +422 -0
- twmarketdata-0.1.0/src/twmd/errors.py +137 -0
- twmarketdata-0.1.0/src/twmd/frames.py +141 -0
- twmarketdata-0.1.0/tests/test_client.py +571 -0
- twmarketdata-0.1.0/tests/test_live_keyfree.py +110 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
sandbox-install:
|
|
11
|
+
# Proves the package installs from wheels alone, with no compiler and no
|
|
12
|
+
# system libraries, across supported Pythons and platforms.
|
|
13
|
+
name: install + offline tests (py${{ matrix.python-version }}, ${{ matrix.os }})
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
19
|
+
python-version: ["3.9", "3.11", "3.13"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
|
|
28
|
+
- name: Install from wheels only
|
|
29
|
+
# --only-binary=:all: fails the build if any dependency would need to be
|
|
30
|
+
# compiled from source, which is the sandbox constraint we must hold.
|
|
31
|
+
run: |
|
|
32
|
+
python -m pip install --upgrade pip
|
|
33
|
+
python -m pip install --only-binary=:all: -e ".[test]"
|
|
34
|
+
|
|
35
|
+
- name: Offline test suite
|
|
36
|
+
# No network: the live suite is deselected.
|
|
37
|
+
run: python -m pytest -m "not live" -q
|
|
38
|
+
|
|
39
|
+
- name: Assert no compiled extensions shipped
|
|
40
|
+
shell: python
|
|
41
|
+
run: |
|
|
42
|
+
import pathlib, sys
|
|
43
|
+
bad = [
|
|
44
|
+
str(p)
|
|
45
|
+
for p in pathlib.Path("src/twmd").rglob("*")
|
|
46
|
+
if p.suffix in {".so", ".pyd", ".dylib", ".c", ".pyx"}
|
|
47
|
+
]
|
|
48
|
+
if bad:
|
|
49
|
+
sys.exit("compiled artefacts found in package: " + ", ".join(bad))
|
|
50
|
+
print("pure Python: ok")
|
|
51
|
+
|
|
52
|
+
dependency-floor:
|
|
53
|
+
# requires-python is >=3.9 and pandas is >=1.5 to maximise the number of
|
|
54
|
+
# environments this installs into. A declared floor nobody tests is a guess,
|
|
55
|
+
# so this job pins the floor exactly and runs the suite against it.
|
|
56
|
+
name: declared dependency floor (py3.9)
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
|
|
61
|
+
- uses: actions/setup-python@v5
|
|
62
|
+
with:
|
|
63
|
+
python-version: "3.9"
|
|
64
|
+
|
|
65
|
+
- name: Install at the declared floor
|
|
66
|
+
# numpy<2 is required alongside pandas 1.5: pandas 1.5 wheels are built
|
|
67
|
+
# against the numpy 1.x C ABI and abort at import under numpy 2 with
|
|
68
|
+
# "numpy.dtype size changed". That is a pandas/numpy ABI constraint, not
|
|
69
|
+
# a twmd one — our code passes the full suite on pandas 1.5 — but the
|
|
70
|
+
# floor must be installed the way it actually works.
|
|
71
|
+
run: |
|
|
72
|
+
python -m pip install --upgrade pip
|
|
73
|
+
python -m pip install --only-binary=:all: "pandas==1.5.0" "numpy<2" "httpx==0.27.0"
|
|
74
|
+
python -m pip install --only-binary=:all: --no-deps -e .
|
|
75
|
+
python -m pip install pytest respx
|
|
76
|
+
|
|
77
|
+
- name: Report resolved versions
|
|
78
|
+
run: |
|
|
79
|
+
python -c "import sys,pandas,numpy,httpx; print(f'py {sys.version.split()[0]} | pandas {pandas.__version__} | numpy {numpy.__version__} | httpx {httpx.__version__}')"
|
|
80
|
+
|
|
81
|
+
- name: Offline test suite at the floor
|
|
82
|
+
run: python -m pytest -m "not live" -q
|
|
83
|
+
|
|
84
|
+
live-keyfree:
|
|
85
|
+
# The zero-configuration promise: five sample tickers, no credentials.
|
|
86
|
+
# Needs egress, so it is a separate job from the sandbox install check.
|
|
87
|
+
name: live key-free acceptance
|
|
88
|
+
runs-on: ubuntu-latest
|
|
89
|
+
steps:
|
|
90
|
+
- uses: actions/checkout@v4
|
|
91
|
+
|
|
92
|
+
- uses: actions/setup-python@v5
|
|
93
|
+
with:
|
|
94
|
+
python-version: "3.11"
|
|
95
|
+
|
|
96
|
+
- name: Install
|
|
97
|
+
run: |
|
|
98
|
+
python -m pip install --upgrade pip
|
|
99
|
+
python -m pip install -e ".[test]"
|
|
100
|
+
|
|
101
|
+
- name: Key-free acceptance against the live API
|
|
102
|
+
# TWMD_API_KEY is deliberately not set: this must pass with no credentials.
|
|
103
|
+
run: python -m pytest -m live -q
|
|
104
|
+
|
|
105
|
+
compliance:
|
|
106
|
+
# Positioning gate: this package is a data-access layer. Nothing in it may
|
|
107
|
+
# read as investment advice, a recommendation, or a directional claim.
|
|
108
|
+
name: compliance scan
|
|
109
|
+
runs-on: ubuntu-latest
|
|
110
|
+
steps:
|
|
111
|
+
- uses: actions/checkout@v4
|
|
112
|
+
|
|
113
|
+
- name: Scan for advisory or directional language
|
|
114
|
+
shell: bash
|
|
115
|
+
run: |
|
|
116
|
+
set -uo pipefail
|
|
117
|
+
# Deliberately blunt. A false positive is cheap to reword; a false
|
|
118
|
+
# negative ships advisory language. Do not loosen this to accommodate
|
|
119
|
+
# a phrasing — change the phrasing.
|
|
120
|
+
#
|
|
121
|
+
# Excluded on purpose: this file (it necessarily contains the terms it
|
|
122
|
+
# bans) and build/cache dirs. Everything else is in scope, including
|
|
123
|
+
# tests, comments and packaging metadata.
|
|
124
|
+
#
|
|
125
|
+
# Not banned, and why: "backtest" and "point-in-time" appear only
|
|
126
|
+
# inside a verbatim quote of the API's own survivorship_bias_warning;
|
|
127
|
+
# banning them would force us to misquote an upstream integrity
|
|
128
|
+
# caveat. "signal" is caught only in its advisory senses below.
|
|
129
|
+
PATTERN='\b(buy|sell|hold) (signal|recommendation|rating)|\brecommend(s|ed|ation|ations)?\b|\bstock (pick|tip)|\bshould (buy|sell|invest)|\bbullish\b|\bbearish\b|\blong/short\b|\bwin.?rate\b|\bprofit(able|ability)\b|\bguarantee|\boutperform|\bprice target|\b(alpha|trade|trading|entry|exit) signal|\btrading strategy|\bwill (rise|fall|go up|go down)|\bbeat the market|\b(under|over)valued\b|\brisk.adjusted\b|\bsharpe\b|\bmarket timing\b|\bscreen for\b|\bedge over\b|\binvestment advice\b'
|
|
130
|
+
# A disclaimer must be able to name what it disclaims. So matches are
|
|
131
|
+
# filtered through a second pass that drops lines carrying an explicit
|
|
132
|
+
# negation — "does not generate recommendations" is the positioning
|
|
133
|
+
# working, not failing. Anything asserted affirmatively still fails.
|
|
134
|
+
NEGATED='\b(no|not|never|nothing|neither|nor|without|deliberately not|does not|do not|is not|are not|may not|should not|free of)\b'
|
|
135
|
+
|
|
136
|
+
HITS=$(grep -rEIn \
|
|
137
|
+
--exclude-dir=.git --exclude-dir=.venv --exclude-dir=.pytest_cache \
|
|
138
|
+
--exclude-dir=__pycache__ --exclude-dir=build --exclude-dir=dist \
|
|
139
|
+
--exclude=ci.yml \
|
|
140
|
+
-i "$PATTERN" . | grep -vEi "$NEGATED" || true)
|
|
141
|
+
|
|
142
|
+
if [ -n "$HITS" ]; then
|
|
143
|
+
echo "$HITS"
|
|
144
|
+
echo "::error::Advisory or directional language found. This package is data access only."
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
echo "no advisory language: ok"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TW Market Data
|
|
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,319 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: twmarketdata
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Data-access client for the TW Market Data API. Retrieval only, for research and education.
|
|
5
|
+
Project-URL: Homepage, https://twmarketdata.com
|
|
6
|
+
Project-URL: Documentation, https://api.twmarketdata.com/openapi.json
|
|
7
|
+
Author: TW Market Data
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 TW Market Data
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: api-client,dataset,market-data,research,taiwan
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: Intended Audience :: Education
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.9
|
|
45
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
46
|
+
Requires-Dist: pandas>=1.5
|
|
47
|
+
Provides-Extra: test
|
|
48
|
+
Requires-Dist: pytest>=7.4; extra == 'test'
|
|
49
|
+
Requires-Dist: respx>=0.21; extra == 'test'
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# twmd
|
|
53
|
+
|
|
54
|
+
Python data-access client for the [TW Market Data](https://twmarketdata.com) API.
|
|
55
|
+
|
|
56
|
+
`twmd` retrieves published datasets over HTTP and returns them as pandas
|
|
57
|
+
DataFrames. It is a transport layer for data retrieval, intended for research
|
|
58
|
+
and educational use. It fetches records as the API publishes them and performs
|
|
59
|
+
no analysis, scoring, ranking, or interpretation of any kind. Deciding what the
|
|
60
|
+
data means, and what if anything to do about it, is entirely the caller's own
|
|
61
|
+
work and responsibility.
|
|
62
|
+
|
|
63
|
+
Responses carry the API's own `lineage.not_investment_advice` flag; this client
|
|
64
|
+
preserves it unmodified.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install twmarketdata
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The distribution is named `twmarketdata`; the import package is `twmd`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import twmd
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Pure Python. Dependencies are `httpx` and `pandas` — no compiled extensions, no
|
|
79
|
+
system libraries, installable in a restricted sandbox.
|
|
80
|
+
|
|
81
|
+
Requires Python 3.9 or newer. The floors are deliberately low (`pandas>=1.5`,
|
|
82
|
+
`httpx>=0.27`) so this installs into existing environments without forcing an
|
|
83
|
+
upgrade, and CI runs the full suite pinned to exactly those floors so they stay
|
|
84
|
+
honest rather than aspirational.
|
|
85
|
+
|
|
86
|
+
One caveat that is not ours to fix: pandas 1.5 wheels are built against the
|
|
87
|
+
numpy 1.x C ABI and fail at import under numpy 2 with `numpy.dtype size
|
|
88
|
+
changed`. If you are pinned to pandas 1.x, pin `numpy<2` alongside it. Anything
|
|
89
|
+
from pandas 2.2.2 onward works with numpy 2 unconstrained.
|
|
90
|
+
|
|
91
|
+
## Quick start — no API key needed
|
|
92
|
+
|
|
93
|
+
Five sample tickers are served without credentials on selected datasets, so
|
|
94
|
+
this runs with zero configuration:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from twmd import Client
|
|
98
|
+
|
|
99
|
+
client = Client()
|
|
100
|
+
df = client.get_dataset("twse-daily-price", symbol="2330", limit=5)
|
|
101
|
+
|
|
102
|
+
print(df[["date", "open", "high", "low", "close", "volume_shares"]])
|
|
103
|
+
print(df.attrs["data_as_of"]) # data freshness date
|
|
104
|
+
print(df.attrs["lineage"]) # provider, source endpoints, source table
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Authentication
|
|
108
|
+
|
|
109
|
+
Set your key in the environment; it is never written to disk or logged:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
export TWMD_API_KEY="sk_live_..."
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`Client()` picks it up automatically. You may also pass `Client(api_key=...)`
|
|
116
|
+
explicitly. With no key set, the client runs in key-free mode and reaches the
|
|
117
|
+
datasets listed below.
|
|
118
|
+
|
|
119
|
+
## Key-free access matrix
|
|
120
|
+
|
|
121
|
+
Key-free access is scoped **per dataset**, not globally per ticker. The same
|
|
122
|
+
ticker that is served on one dataset may require a key on another. Measured
|
|
123
|
+
against the live API on 2026-07-21:
|
|
124
|
+
|
|
125
|
+
| Tier | Datasets | Tickers served without a key |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| Open | `security-master`, `market-index` | any |
|
|
128
|
+
| Sample | `twse-daily-price`, `tpex-daily-price`, `monthly-revenue` | `2330`, `2317`, `2454`, `0050`, `2603` only |
|
|
129
|
+
| Key required | everything else, including `institutional-flow`, `market-prices`, `financial-metrics`, `income-statement`, `balance-sheet` | none |
|
|
130
|
+
|
|
131
|
+
Check before requesting:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from twmd import is_key_free
|
|
135
|
+
|
|
136
|
+
is_key_free("twse-daily-price", "2330") # True
|
|
137
|
+
is_key_free("twse-daily-price", "1101") # False — outside the sample set
|
|
138
|
+
is_key_free("security-master", "1101") # True — open dataset
|
|
139
|
+
is_key_free("institutional-flow", "2330") # False — key required
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Datasets not in the table are treated as key-required, which is the safe
|
|
143
|
+
default. The client never blocks a request on this basis — it only uses the
|
|
144
|
+
matrix to explain a 401 after the fact.
|
|
145
|
+
|
|
146
|
+
## DataFrames and metadata
|
|
147
|
+
|
|
148
|
+
Records become rows. Everything else in the response envelope is preserved on
|
|
149
|
+
`df.attrs`.
|
|
150
|
+
|
|
151
|
+
The API uses **two envelope shapes**, and `twmd` handles both transparently:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# rows / count — twse-daily-price, tpex-daily-price, monthly-revenue
|
|
155
|
+
df.attrs["dataset"] # "twse_daily_price"
|
|
156
|
+
df.attrs["count"] # record count
|
|
157
|
+
df.attrs["data_as_of"] # data freshness date
|
|
158
|
+
df.attrs["source_role"] # e.g. "official_twse"
|
|
159
|
+
df.attrs["lineage"] # provider, official_source, source_endpoints, table
|
|
160
|
+
df.attrs["meta"] # last_trading_day, market_status
|
|
161
|
+
|
|
162
|
+
# items / row_count — security-master, market-index
|
|
163
|
+
df.attrs["dataset_id"] # "security-master"
|
|
164
|
+
df.attrs["row_count"] # record count
|
|
165
|
+
df.attrs["as_of_date"] # snapshot date
|
|
166
|
+
df.attrs["survivorship_bias_warning"] # integrity caveat raised by the API
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Envelope contents vary by dataset — `monthly-revenue` sends only `dataset`,
|
|
170
|
+
`rows` and `count` — so read `attrs` defensively.
|
|
171
|
+
|
|
172
|
+
Records in the `items` variant contain nested objects (`security_identity`,
|
|
173
|
+
`market_identity`, `index_level`). These stay as dict-valued columns rather than
|
|
174
|
+
being flattened, so a DataFrame reflects what the API actually sent. Expand one
|
|
175
|
+
when you want to:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import pandas as pd
|
|
179
|
+
identity = pd.json_normalize(df["security_identity"])
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Note that `security-master` carries a `survivorship_bias_warning` stating the
|
|
183
|
+
current master is not point-in-time complete. It is surfaced on `attrs`
|
|
184
|
+
unmodified; check it before using that dataset for historical work.
|
|
185
|
+
|
|
186
|
+
## Errors
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from twmd import Client, TwmdAuthError, TwmdPaymentRequired
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
df = client.get_dataset("institutional-flow", symbol="2330")
|
|
193
|
+
except TwmdAuthError as exc:
|
|
194
|
+
print(exc.error_code) # "missing_api_key" or "invalid_api_key"
|
|
195
|
+
print(exc.body) # decoded response body, verbatim
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Every `TwmdAPIError` subclass — every row in the table below except the last —
|
|
199
|
+
exposes `.status_code`, `.body` (decoded body, unmodified), `.text` and
|
|
200
|
+
`.error_code`. `TwmdTransportError` and `TwmdConfigError` derive from `TwmdError`
|
|
201
|
+
directly and carry none of those, since no response was received.
|
|
202
|
+
|
|
203
|
+
| Status | Exception | Retried |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| 401 | `TwmdAuthError` | no |
|
|
206
|
+
| 402 | `TwmdPaymentRequired` | no |
|
|
207
|
+
| 404 | `TwmdNotFoundError` | no |
|
|
208
|
+
| 422 | `TwmdValidationError` | no |
|
|
209
|
+
| 429 | `TwmdRateLimitError` | yes |
|
|
210
|
+
| 5xx | `TwmdServerError` | yes |
|
|
211
|
+
| network failure | `TwmdTransportError` | yes |
|
|
212
|
+
|
|
213
|
+
Retries use exponential backoff with jitter. A `Retry-After` header — in either
|
|
214
|
+
RFC 7231 form, delta-seconds or HTTP-date — overrides that and is honoured
|
|
215
|
+
exactly, with no jitter and no shortening, since retrying sooner than the server
|
|
216
|
+
permitted is worse than waiting too long. If it asks for more than
|
|
217
|
+
`RETRY_AFTER_MAX` (120s) the client stops and raises the server's error rather
|
|
218
|
+
than blocking for minutes.
|
|
219
|
+
|
|
220
|
+
401 messages are annotated with the key-free status of the dataset and ticker
|
|
221
|
+
you asked for, so "I forgot my key" is distinguishable from "that ticker is not
|
|
222
|
+
in the sample set".
|
|
223
|
+
|
|
224
|
+
### On 402
|
|
225
|
+
|
|
226
|
+
**No 402 response has been observed from this API.** The probe on 2026-07-21
|
|
227
|
+
found zero endpoints declaring 402 in the published OpenAPI document, and
|
|
228
|
+
unauthenticated requests to key-gated datasets return 401 `missing_api_key`.
|
|
229
|
+
`TwmdPaymentRequired` exists as a documented extension point: if the API does
|
|
230
|
+
return 402 — the untested path is a *valid but unentitled* key, which requires
|
|
231
|
+
live credentials to exercise — the full decoded body is preserved verbatim on
|
|
232
|
+
`.body` and reaches your code intact. The client deliberately does not model or
|
|
233
|
+
require any field layout inside it, since no 402 body exists to model.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
except TwmdPaymentRequired as exc:
|
|
237
|
+
handle_payment_required(exc.body) # whole body, nothing dropped
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Source attribution
|
|
241
|
+
|
|
242
|
+
Pass `source` to mark every request with the integration that produced it, so
|
|
243
|
+
the publisher can attribute traffic:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
client = Client(source="ecosys/tradingagents")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
It is attached as a `source` query parameter on every request. A per-call
|
|
250
|
+
`source=` on `get_dataset` overrides the client-wide value. It is an ordinary
|
|
251
|
+
query parameter — it does not change the response, does not enter a data
|
|
252
|
+
response's `request_context.filters`, and carries nothing about the user. Leave
|
|
253
|
+
it unset to send nothing.
|
|
254
|
+
|
|
255
|
+
## Pagination
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
df = client.get_all("twse-daily-price", symbol="2330", limit=1000)
|
|
259
|
+
|
|
260
|
+
for page in client.iter_pages("twse-daily-price", symbol="2330", limit=500):
|
|
261
|
+
process(page)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The API publishes no cursor: no `cursor` or `next_cursor` field appears in any
|
|
265
|
+
response or in the OpenAPI document. Paging is `limit`/`offset`, and `offset`
|
|
266
|
+
was measured to be **ignored** on the key-free endpoints — `offset=3` returned
|
|
267
|
+
the same records as `offset=0`.
|
|
268
|
+
|
|
269
|
+
So pagination here is defensive. It advances `offset` as documented, then stops
|
|
270
|
+
when either a page comes back shorter than `limit`, or a page repeats the
|
|
271
|
+
previous page's first record — the signature of a silently ignored `offset`.
|
|
272
|
+
Against endpoints that ignore `offset` this yields exactly one page, which is
|
|
273
|
+
the correct outcome rather than a failure. `max_pages` caps the loop.
|
|
274
|
+
|
|
275
|
+
## `as_of`
|
|
276
|
+
|
|
277
|
+
`get_dataset()` accepts an `as_of` argument and forwards it as a query
|
|
278
|
+
parameter. **Its scope is narrow.** As measured on 2026-07-21:
|
|
279
|
+
|
|
280
|
+
- `as_of` is declared on exactly four endpoints — `income-statement`,
|
|
281
|
+
`cash-flow-statement`, `balance-sheet`, `financials` — all of which require an
|
|
282
|
+
API key.
|
|
283
|
+
- On every other endpoint it is not a declared parameter. The API silently
|
|
284
|
+
ignores unknown query parameters, returning 200 rather than 422, so passing
|
|
285
|
+
`as_of` there has no observable effect and raises no error.
|
|
286
|
+
- Its behaviour on the four declared endpoints is **unverified** by this
|
|
287
|
+
project, which has no credentials to exercise them.
|
|
288
|
+
|
|
289
|
+
Treat `as_of` as forwarded-but-unconfirmed rather than as a general
|
|
290
|
+
point-in-time facility. The `data_as_of` field in responses is a data-freshness
|
|
291
|
+
date and is a different thing.
|
|
292
|
+
|
|
293
|
+
## Development
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
pip install -e ".[test]"
|
|
297
|
+
pytest -m "not live" # offline, no network
|
|
298
|
+
pytest -m live # hits the real API, key-free paths only
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The live suite asserts a sample of the key-free matrix — seven dataset/ticker
|
|
302
|
+
pairs — still matches what the API serves, so drift in those surfaces as a test
|
|
303
|
+
failure. It is a tripwire, not full coverage: the five sample tickers are
|
|
304
|
+
verified only against `twse-daily-price`, and three datasets listed as
|
|
305
|
+
key-required are assumed rather than probed (see
|
|
306
|
+
`access.PRESUMED_KEY_REQUIRED_DATASETS`, and `access.provenance()` to tell the
|
|
307
|
+
two apart).
|
|
308
|
+
|
|
309
|
+
## Scope
|
|
310
|
+
|
|
311
|
+
This package retrieves data. It does not generate recommendations, forecasts,
|
|
312
|
+
signals, valuations, or opinions about any security, and nothing it returns
|
|
313
|
+
should be read as such. The data is provided for research and educational
|
|
314
|
+
purposes; verify it against the original sources before relying on it. Use of
|
|
315
|
+
the underlying API is governed by TW Market Data's own terms.
|
|
316
|
+
|
|
317
|
+
## License
|
|
318
|
+
|
|
319
|
+
MIT
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# PyPI 發布指令包 — `twmarketdata`(owner 執行)
|
|
2
|
+
|
|
3
|
+
**核准**:owner 已核准 PyPI 發布(2026-07-21)。
|
|
4
|
+
**發布名**:`twmarketdata`(PyPI 400 拒絕 `twmd`:與既有專案過於相似)。**import 名仍是 `twmd`**——零程式碼改動,使用者 `pip install twmarketdata` 後 `import twmd`。
|
|
5
|
+
**原則**:以下全部 ⟦OWNER 執行⟧。帳號與 API token 為 owner 親辦憑證,不經終端/agent。**同一個先前建的 token 可續用**(account-scope)。
|
|
6
|
+
**已驗**:agent 已改名後重新產出並驗證 artifacts(`python -m build` 成功、`twine check` 兩檔 PASSED、wheel 純 Python 無編譯物、含 LICENSE、import 名不變)。owner 只需憑證 + 上傳。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 前置 A — PyPI 帳號(若已有,跳到前置 B)⟦OWNER⟧
|
|
11
|
+
|
|
12
|
+
1. 註冊:**https://pypi.org/account/register/**
|
|
13
|
+
2. 開啟 2FA(PyPI 現在強制):帳號設定 → Two factor authentication
|
|
14
|
+
3. (建議)先在 TestPyPI 也註冊一個,供試上傳:**https://test.pypi.org/account/register/**
|
|
15
|
+
|
|
16
|
+
## 前置 B — API token ⟦OWNER⟧
|
|
17
|
+
|
|
18
|
+
1. 開 **https://pypi.org/manage/account/token/**
|
|
19
|
+
2. Add API token
|
|
20
|
+
- Token name:`twmarketdata-publish`
|
|
21
|
+
- Scope:首次發布選 **"Entire account"**(因為 `twmarketdata` 專案尚不存在,無法限定專案 scope);首次上傳後可回來刪掉、改發一個 project-scoped token 給後續版本用。**先前為 `twmd` 建的 account-scope token 仍可直接用於本次上傳。**
|
|
22
|
+
3. 複製 token(`pypi-...` 開頭,**只顯示一次**)
|
|
23
|
+
|
|
24
|
+
> token 就是上傳憑證。不要貼進任何檔案或終端歷史。下方用互動式輸入或環境變數當場帶入。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 名稱可用性檢查 ⟦OWNER,建議先做⟧
|
|
29
|
+
|
|
30
|
+
`twmarketdata` 這名字在 PyPI 是否可用(前一次 `twmd` 被相似度規則擋下,已改此名):
|
|
31
|
+
```
|
|
32
|
+
# 開瀏覽器看,404 = 可用
|
|
33
|
+
open https://pypi.org/project/twmarketdata/
|
|
34
|
+
```
|
|
35
|
+
若這名字也被相似度規則擋(上傳時回 400)→ 回報 agent,再擇名(例如 `tw-market-data`),agent 同步改 `pyproject.toml` 的 `name`、README、§3 provider 的 `pip install` 指示後重建。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 發布 ⟦OWNER 在終端執行⟧
|
|
40
|
+
|
|
41
|
+
工作目錄與已驗 artifacts:
|
|
42
|
+
```
|
|
43
|
+
cd /Volumes/DEV_USB/Projects/twmd-python-client
|
|
44
|
+
ls dist/
|
|
45
|
+
# twmarketdata-0.1.0-py3-none-any.whl
|
|
46
|
+
# twmarketdata-0.1.0.tar.gz
|
|
47
|
+
```
|
|
48
|
+
> 若 `dist/` 仍見舊的 `twmd-0.1.0.*`,先 `rm -rf dist/twmd-0.1.0.*`——只上傳 `twmarketdata-*`。
|
|
49
|
+
|
|
50
|
+
### 選項 1(建議):先上 TestPyPI 演練,確認無誤再上正式
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
# 用已建好的 .venv 的 twine(agent 已裝)
|
|
54
|
+
.venv/bin/python -m twine upload --repository testpypi dist/*
|
|
55
|
+
# Username: __token__
|
|
56
|
+
# Password: <貼上 TestPyPI 的 API token>
|
|
57
|
+
|
|
58
|
+
# 驗證 TestPyPI 能裝(乾淨環境)— 裝 twmarketdata、import twmd
|
|
59
|
+
python3 -m venv /tmp/twmd-test && \
|
|
60
|
+
/tmp/twmd-test/bin/pip install --index-url https://test.pypi.org/simple/ \
|
|
61
|
+
--extra-index-url https://pypi.org/simple/ twmarketdata && \
|
|
62
|
+
/tmp/twmd-test/bin/python -c "import twmd; print(twmd.__version__)"
|
|
63
|
+
```
|
|
64
|
+
> `--extra-index-url` 讓依賴(httpx/pandas)從正式 PyPI 抓,因為 TestPyPI 未必有。
|
|
65
|
+
|
|
66
|
+
### 選項 2 或演練後:上正式 PyPI
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
cd /Volumes/DEV_USB/Projects/twmd-python-client
|
|
70
|
+
.venv/bin/python -m twine upload dist/*
|
|
71
|
+
# Username: __token__
|
|
72
|
+
# Password: <貼上正式 PyPI 的 API token>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
若偏好環境變數(避免互動貼上):
|
|
76
|
+
```
|
|
77
|
+
export TWINE_USERNAME=__token__
|
|
78
|
+
export TWINE_PASSWORD=pypi-XXXX... # 正式 token
|
|
79
|
+
.venv/bin/python -m twine upload dist/*
|
|
80
|
+
unset TWINE_PASSWORD # 用完清掉
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 發布後確認 ⟦OWNER⟧
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
open https://pypi.org/project/twmarketdata/ # 頁面出現、README 店面正確
|
|
89
|
+
# 從任意乾淨環境驗證公開可裝(裝 twmarketdata、import twmd):
|
|
90
|
+
python3 -m venv /tmp/twmd-live && \
|
|
91
|
+
/tmp/twmd-live/bin/pip install twmarketdata && \
|
|
92
|
+
/tmp/twmd-live/bin/python -c "import twmd; print('live:', twmd.__version__)"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
回報 agent「twmarketdata 0.1.0 已上 PyPI」→ agent 更新 memory,並確認 §3 TA PR 的 `pip install twmarketdata` 前置已滿足。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 重發/改版注意(記著)
|
|
100
|
+
|
|
101
|
+
- PyPI **同一版本號不可覆蓋**。若發現需修,改 `pyproject.toml` 的 `version`(例如 `0.1.1`),重跑 `python -m build` 再上傳。
|
|
102
|
+
- 若首次用 account-scoped token:上傳成功後到 token 頁刪它,改發 `twmd` project-scoped token 給後續版本,縮小憑證範圍。
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## agent 已完成(owner 無需重做)
|
|
107
|
+
|
|
108
|
+
- [x] `pyproject.toml` 元資料齊備(name/version/description/readme/license/requires-python)
|
|
109
|
+
- [x] LICENSE 檔就位,打包進 wheel
|
|
110
|
+
- [x] `python -m build` → wheel + sdist 成功
|
|
111
|
+
- [x] `twine check dist/*` → 兩檔 PASSED
|
|
112
|
+
- [x] wheel 內容驗證:純 Python、零編譯物、含 LICENSE、5 模組
|
|
113
|
+
- [x] 依賴僅 httpx + pandas(純 pip、sandbox 友善)
|