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.
@@ -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,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .coverage
10
+ htmlcov/
11
+ .DS_Store
12
+ .env
@@ -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 友善)