pytdxfeed 0.2.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,8 @@
1
+ .DS_Store
2
+ .pytest_cache/
3
+ .ruff_cache/
4
+ .venv/
5
+ __pycache__/
6
+ *.py[cod]
7
+ dist/
8
+
@@ -0,0 +1,24 @@
1
+ # AGENTS.md
2
+
3
+ ## Environment
4
+
5
+ - This repository uses uv exclusively. Use `uv sync`, `uv run`, `uv lock`, and
6
+ `uv build`; do not add Poetry, pip, pipx, or repository-local environment
7
+ managers.
8
+ - The package is private and Git-only. Do not publish it or its bundled
9
+ tdx-api binaries to PyPI or a public release while the upstream repository
10
+ has no declared license.
11
+ - Before pushing, run `uv run ruff check .`, `uv run ruff format --check .`,
12
+ `uv run pytest`, and `uv build`.
13
+
14
+ ## Runtime contract
15
+
16
+ - The service host is fixed at `127.0.0.1`; the explicit service port defaults
17
+ to `8080` and may be configured by consumers. Do not add configurable hosts,
18
+ base URLs, automatic free-port selection, or alternate service fallback.
19
+ - The macOS runtime root is fixed at `~/.pytdxfeed` and the LaunchAgent label is
20
+ `com.hermanzhaozzzz.pytdxfeed.tdx-api`.
21
+ - Never kill or reuse an unhealthy unknown configured-port owner. A strictly
22
+ healthy unknown tdx-api service on the configured port may be used without
23
+ taking ownership.
24
+ - Market-data requests have no source fallback and no silent retry.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytdxfeed
3
+ Version: 0.2.0
4
+ Summary: Private macOS runtime and HTTP client for oficcejo/tdx-api.
5
+ Author-email: Huanan Herman Zhao <hermanzhaozzzz@gmail.com>
6
+ Requires-Python: <3.14,>=3.11
7
+ Requires-Dist: requests<3.0.0,>=2.32.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # pytdxfeed
11
+
12
+ `pytdxfeed` is a private Python wrapper around the local
13
+ [`oficcejo/tdx-api`](https://github.com/oficcejo/tdx-api) service used by VQT
14
+ and `vnpy_ths`. It provides one macOS LaunchAgent and one HTTP client contract,
15
+ so both consumers share the same process instead of deploying competing copies.
16
+
17
+ The package is installed only from the private Git repository. It is not the
18
+ unrelated package formerly published under similar names on PyPI.
19
+
20
+ ## Runtime
21
+
22
+ - Host: `127.0.0.1`
23
+ - Port: `8080` by default; consumers may select another explicit port
24
+ - Runtime root: `~/.pytdxfeed`
25
+ - LaunchAgent: `com.hermanzhaozzzz.pytdxfeed.tdx-api`
26
+ - Pinned upstream commit: `ea07dccb67aeb92ebde851ac31503b4bf457a318`
27
+
28
+ On macOS, `ensure_service()` reuses an already healthy service or installs the
29
+ bundled binary for the current architecture. The persistent upstream database
30
+ is kept in `~/.pytdxfeed/data/database`. On other systems the function only
31
+ checks an existing local service.
32
+
33
+ ```python
34
+ from pytdxfeed import TdxApiClient, ensure_service
35
+
36
+ service = ensure_service(port=18080)
37
+ with TdxApiClient(timeout_sec=3.0, port=18080) as client:
38
+ health = client.health()
39
+ quotes = client.batch_quote(["sh600000", "sz159915"])
40
+ ```
41
+
42
+ The host and LaunchAgent identity stay fixed. Port selection is explicit: the
43
+ runtime never scans for a free port, and an unhealthy unknown process occupying
44
+ the selected port causes startup to fail without terminating that process. The
45
+ managed macOS service receives the selected port through `TDX_API_PORT`.
46
+
47
+ ## Development
48
+
49
+ ```bash
50
+ uv sync --locked --all-groups
51
+ uv run ruff check .
52
+ uv run ruff format --check .
53
+ uv run pytest
54
+ uv build
55
+ ```
56
+
57
+ Regenerate the private macOS assets when the locked upstream commit or reviewed
58
+ private patch set changes:
59
+
60
+ ```bash
61
+ uv run python scripts/build_assets.py --source /path/to/tdx-api
62
+ ```
63
+
64
+ The build verifies the exact clean upstream checkout, applies the patches in
65
+ `patches/`, runs their Go tests, and records every patch checksum in the asset
66
+ manifest. Artifact generation 3 retains explicit THS JSONP validation and adds
67
+ strict `TDX_API_PORT` handling to the Go service. It does not retry or switch
68
+ market-data sources.
69
+
70
+ The upstream repository currently has no declared license. This repository and
71
+ its derived binaries must remain private/internal until redistribution terms are
72
+ clear.
@@ -0,0 +1,63 @@
1
+ # pytdxfeed
2
+
3
+ `pytdxfeed` is a private Python wrapper around the local
4
+ [`oficcejo/tdx-api`](https://github.com/oficcejo/tdx-api) service used by VQT
5
+ and `vnpy_ths`. It provides one macOS LaunchAgent and one HTTP client contract,
6
+ so both consumers share the same process instead of deploying competing copies.
7
+
8
+ The package is installed only from the private Git repository. It is not the
9
+ unrelated package formerly published under similar names on PyPI.
10
+
11
+ ## Runtime
12
+
13
+ - Host: `127.0.0.1`
14
+ - Port: `8080` by default; consumers may select another explicit port
15
+ - Runtime root: `~/.pytdxfeed`
16
+ - LaunchAgent: `com.hermanzhaozzzz.pytdxfeed.tdx-api`
17
+ - Pinned upstream commit: `ea07dccb67aeb92ebde851ac31503b4bf457a318`
18
+
19
+ On macOS, `ensure_service()` reuses an already healthy service or installs the
20
+ bundled binary for the current architecture. The persistent upstream database
21
+ is kept in `~/.pytdxfeed/data/database`. On other systems the function only
22
+ checks an existing local service.
23
+
24
+ ```python
25
+ from pytdxfeed import TdxApiClient, ensure_service
26
+
27
+ service = ensure_service(port=18080)
28
+ with TdxApiClient(timeout_sec=3.0, port=18080) as client:
29
+ health = client.health()
30
+ quotes = client.batch_quote(["sh600000", "sz159915"])
31
+ ```
32
+
33
+ The host and LaunchAgent identity stay fixed. Port selection is explicit: the
34
+ runtime never scans for a free port, and an unhealthy unknown process occupying
35
+ the selected port causes startup to fail without terminating that process. The
36
+ managed macOS service receives the selected port through `TDX_API_PORT`.
37
+
38
+ ## Development
39
+
40
+ ```bash
41
+ uv sync --locked --all-groups
42
+ uv run ruff check .
43
+ uv run ruff format --check .
44
+ uv run pytest
45
+ uv build
46
+ ```
47
+
48
+ Regenerate the private macOS assets when the locked upstream commit or reviewed
49
+ private patch set changes:
50
+
51
+ ```bash
52
+ uv run python scripts/build_assets.py --source /path/to/tdx-api
53
+ ```
54
+
55
+ The build verifies the exact clean upstream checkout, applies the patches in
56
+ `patches/`, runs their Go tests, and records every patch checksum in the asset
57
+ manifest. Artifact generation 3 retains explicit THS JSONP validation and adds
58
+ strict `TDX_API_PORT` handling to the Go service. It does not retry or switch
59
+ market-data sources.
60
+
61
+ The upstream repository currently has no declared license. This repository and
62
+ its derived binaries must remain private/internal until redistribution terms are
63
+ clear.
@@ -0,0 +1,85 @@
1
+ diff --git a/web/server.go b/web/server.go
2
+ --- a/web/server.go
3
+ +++ b/web/server.go
4
+ @@ -8,5 +8,6 @@ import (
5
+ "net/http"
6
+ "os"
7
+ "path/filepath"
8
+ + "strconv"
9
+ "strings"
10
+ "time"
11
+ @@ -804,7 +805,25 @@ func main() {
12
+ http.HandleFunc("/api/tasks", handleListTasks)
13
+ http.HandleFunc("/api/tasks/", handleTaskOperations)
14
+
15
+ - port := ":8080"
16
+ - log.Printf("服务启动成功,访问 http://localhost%s\n", port)
17
+ - log.Fatal(http.ListenAndServe(port, nil))
18
+ + address, err := resolveListenAddress()
19
+ + if err != nil {
20
+ + log.Fatalf("服务端口配置无效: %v", err)
21
+ + }
22
+ + log.Printf("服务启动成功,访问 http://localhost%s\n", address)
23
+ + log.Fatal(http.ListenAndServe(address, nil))
24
+ }
25
+ +
26
+ +func resolveListenAddress() (string, error) {
27
+ + raw, configured := os.LookupEnv("TDX_API_PORT")
28
+ + if !configured || raw == "" {
29
+ + return ":8080", nil
30
+ + }
31
+ + if raw != strings.TrimSpace(raw) {
32
+ + return "", fmt.Errorf("TDX_API_PORT必须是1到65535之间的整数")
33
+ + }
34
+ + port, err := strconv.Atoi(raw)
35
+ + if err != nil || port < 1 || port > 65535 {
36
+ + return "", fmt.Errorf("TDX_API_PORT必须是1到65535之间的整数")
37
+ + }
38
+ + return fmt.Sprintf(":%d", port), nil
39
+ +}
40
+ diff --git a/web/server_port_test.go b/web/server_port_test.go
41
+ new file mode 100644
42
+ --- /dev/null
43
+ +++ b/web/server_port_test.go
44
+ @@ -0,0 +1,41 @@
45
+ +package main
46
+ +
47
+ +import "testing"
48
+ +
49
+ +func TestResolveListenAddress(t *testing.T) {
50
+ + tests := []struct {
51
+ + name string
52
+ + value string
53
+ + set bool
54
+ + want string
55
+ + wantErr bool
56
+ + }{
57
+ + {name: "unset", want: ":8080"},
58
+ + {name: "empty", value: "", set: true, want: ":8080"},
59
+ + {name: "custom", value: "18080", set: true, want: ":18080"},
60
+ + {name: "zero", value: "0", set: true, wantErr: true},
61
+ + {name: "too large", value: "65536", set: true, wantErr: true},
62
+ + {name: "text", value: "http", set: true, wantErr: true},
63
+ + {name: "whitespace", value: " 18080", set: true, wantErr: true},
64
+ + }
65
+ + for _, test := range tests {
66
+ + t.Run(test.name, func(t *testing.T) {
67
+ + if test.set {
68
+ + t.Setenv("TDX_API_PORT", test.value)
69
+ + }
70
+ + got, err := resolveListenAddress()
71
+ + if test.wantErr {
72
+ + if err == nil {
73
+ + t.Fatalf("expected error, got %q", got)
74
+ + }
75
+ + return
76
+ + }
77
+ + if err != nil {
78
+ + t.Fatalf("unexpected error: %v", err)
79
+ + }
80
+ + if got != test.want {
81
+ + t.Fatalf("got %q, want %q", got, test.want)
82
+ + }
83
+ + })
84
+ + }
85
+ +}
@@ -0,0 +1,101 @@
1
+ diff --git a/extend/spider-ths.go b/extend/spider-ths.go
2
+ --- a/extend/spider-ths.go
3
+ +++ b/extend/spider-ths.go
4
+ @@ -124,14 +124,19 @@ func GetTHSDayKline(code string, _type uint8) ([]*Kline, error) {
5
+ return nil, err
6
+ }
7
+
8
+ defer resp.Body.Close()
9
+ + if resp.StatusCode != http.StatusOK {
10
+ + return nil, fmt.Errorf("同花顺日K线HTTP状态异常: %s", resp.Status)
11
+ + }
12
+ bs, err := io.ReadAll(resp.Body)
13
+ if err != nil {
14
+ return nil, err
15
+ }
16
+
17
+ - n := bytes.IndexByte(bs, '(')
18
+ - bs = bs[n+1 : len(bs)-1]
19
+ + bs, err = parseTHSJSONPBody(bs)
20
+ + if err != nil {
21
+ + return nil, err
22
+ + }
23
+
24
+ m := map[string]any{}
25
+ err = json.Unmarshal(bs, &m)
26
+ @@ -196,3 +217,20 @@ func GetTHSDayKline(code string, _type uint8) ([]*Kline, error) {
27
+
28
+ return ls, nil
29
+ }
30
+ +
31
+ +func parseTHSJSONPBody(bs []byte) ([]byte, error) {
32
+ + trimmed := bytes.TrimSpace(bs)
33
+ + if len(trimmed) == 0 {
34
+ + return nil, fmt.Errorf("同花顺日K线返回空响应")
35
+ + }
36
+ + open := bytes.IndexByte(trimmed, '(')
37
+ + close := bytes.LastIndexByte(trimmed, ')')
38
+ + if open < 0 || close <= open {
39
+ + return nil, fmt.Errorf("同花顺日K线返回无效JSONP: bytes=%d", len(trimmed))
40
+ + }
41
+ + payload := bytes.TrimSpace(trimmed[open+1 : close])
42
+ + if len(payload) == 0 {
43
+ + return nil, fmt.Errorf("同花顺日K线JSONP内容为空")
44
+ + }
45
+ + return payload, nil
46
+ +}
47
+ diff --git a/extend/spider-ths_test.go b/extend/spider-ths_test.go
48
+ --- a/extend/spider-ths_test.go
49
+ +++ b/extend/spider-ths_test.go
50
+ @@ -1,16 +1,43 @@
51
+ package extend
52
+
53
+ import (
54
+ + "bytes"
55
+ "testing"
56
+ )
57
+
58
+ -func TestNewSpiderTHS(t *testing.T) {
59
+ - ls, err := GetTHSDayKline("sz000001", THS_HFQ)
60
+ - if err != nil {
61
+ - t.Error(err)
62
+ - return
63
+ - }
64
+ - for _, v := range ls {
65
+ - t.Log(v)
66
+ +func TestParseTHSJSONPBody(t *testing.T) {
67
+ + tests := []struct {
68
+ + name string
69
+ + body string
70
+ + want string
71
+ + wantErr bool
72
+ + }{
73
+ + {name: "empty", body: "", wantErr: true},
74
+ + {name: "whitespace", body: " \n\t", wantErr: true},
75
+ + {name: "missing opener", body: `callback{"status":"ok"})`, wantErr: true},
76
+ + {name: "missing closer", body: `callback({"status":"ok"}`, wantErr: true},
77
+ + {name: "empty payload", body: "callback()", wantErr: true},
78
+ + {
79
+ + name: "valid with trailing semicolon",
80
+ + body: " callback({\"status\":\"ok\"});\n",
81
+ + want: `{"status":"ok"}`,
82
+ + },
83
+ + }
84
+ + for _, test := range tests {
85
+ + t.Run(test.name, func(t *testing.T) {
86
+ + got, err := parseTHSJSONPBody([]byte(test.body))
87
+ + if test.wantErr {
88
+ + if err == nil {
89
+ + t.Fatalf("expected error, got %q", got)
90
+ + }
91
+ + return
92
+ + }
93
+ + if err != nil {
94
+ + t.Fatalf("unexpected error: %v", err)
95
+ + }
96
+ + if !bytes.Equal(got, []byte(test.want)) {
97
+ + t.Fatalf("got %q, want %q", got, test.want)
98
+ + }
99
+ + })
100
+ }
101
+ }
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "pytdxfeed"
3
+ version = "0.2.0"
4
+ description = "Private macOS runtime and HTTP client for oficcejo/tdx-api."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11,<3.14"
7
+ authors = [
8
+ { name = "Huanan Herman Zhao", email = "hermanzhaozzzz@gmail.com" },
9
+ ]
10
+ dependencies = [
11
+ "requests>=2.32.0,<3.0.0",
12
+ ]
13
+
14
+ [dependency-groups]
15
+ dev = [
16
+ "pytest>=8.0.0,<9.0.0",
17
+ "ruff>=0.12.0,<1.0.0",
18
+ ]
19
+
20
+ [build-system]
21
+ requires = ["hatchling>=1.27.0,<2.0.0"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/pytdxfeed"]
26
+
27
+ [tool.hatch.build.targets.sdist]
28
+ include = [
29
+ "/src",
30
+ "/tests",
31
+ "/scripts",
32
+ "/patches",
33
+ "/README.md",
34
+ "/AGENTS.md",
35
+ "/pyproject.toml",
36
+ "/uv.lock",
37
+ ]
38
+
39
+ [tool.ruff]
40
+ line-length = 88
41
+ target-version = "py311"
42
+
43
+ [tool.ruff.lint]
44
+ select = ["A", "B", "C4", "D", "E", "F", "I", "N", "PERF", "RUF", "S", "SIM", "UP", "W"]
45
+ ignore = ["D105", "D107", "D203", "D213"]
46
+
47
+ [tool.ruff.lint.per-file-ignores]
48
+ "tests/**" = ["D", "S101", "S108"]
49
+ "scripts/**" = ["D103", "S603"]
50
+
51
+ [tool.pytest.ini_options]
52
+ addopts = "-q"
53
+ testpaths = ["tests"]
@@ -0,0 +1,193 @@
1
+ """Build deterministic private macOS tdx-api assets for pytdxfeed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import gzip
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import tarfile
14
+ import tempfile
15
+ from pathlib import Path
16
+
17
+ UPSTREAM_URL = "https://github.com/oficcejo/tdx-api.git"
18
+ UPSTREAM_SHA = "ea07dccb67aeb92ebde851ac31503b4bf457a318"
19
+ ARTIFACT_GENERATION = 3
20
+ REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
21
+ PATCH_FILES = (
22
+ REPOSITORY_ROOT / "patches" / "tdx-api-ths-response-validation.patch",
23
+ REPOSITORY_ROOT / "patches" / "tdx-api-configurable-port.patch",
24
+ )
25
+ GO_VERSION_RE = re.compile(r"\bgo(\d+)\.(\d+)(?:\.\d+)?\b")
26
+ ARCHITECTURES = ("arm64", "x86_64")
27
+ GO_ARCHITECTURES = {"arm64": "arm64", "x86_64": "amd64"}
28
+
29
+
30
+ def main() -> None:
31
+ parser = argparse.ArgumentParser()
32
+ parser.add_argument("--source", type=Path)
33
+ parser.add_argument(
34
+ "--output",
35
+ type=Path,
36
+ default=Path(__file__).resolve().parents[1] / "src" / "pytdxfeed" / "assets",
37
+ )
38
+ args = parser.parse_args()
39
+
40
+ go = _require_go()
41
+ args.output.mkdir(parents=True, exist_ok=True)
42
+ with tempfile.TemporaryDirectory(prefix="pytdxfeed-assets-") as temp_text:
43
+ temp = Path(temp_text)
44
+ source = args.source.expanduser().resolve() if args.source else temp / "source"
45
+ if args.source is None:
46
+ _run(["git", "clone", "--no-checkout", UPSTREAM_URL, str(source)])
47
+ _run(["git", "-C", str(source), "checkout", "--detach", UPSTREAM_SHA])
48
+ _verify_source(source)
49
+ _apply_patches(source)
50
+ _run([str(go), "test", "./extend"], cwd=source)
51
+ _run([str(go), "test", "./..."], cwd=source / "web")
52
+
53
+ assets: dict[str, dict[str, str]] = {}
54
+ for architecture in ARCHITECTURES:
55
+ root = temp / f"tdx-api-{architecture}"
56
+ binary = root / "bin" / "tdx-api"
57
+ binary.parent.mkdir(parents=True)
58
+ environment = {
59
+ **os.environ,
60
+ "CGO_ENABLED": "0",
61
+ "GOOS": "darwin",
62
+ "GOARCH": GO_ARCHITECTURES[architecture],
63
+ }
64
+ _run(
65
+ [
66
+ str(go),
67
+ "build",
68
+ "-trimpath",
69
+ "-ldflags=-s -w",
70
+ "-o",
71
+ str(binary),
72
+ ".",
73
+ ],
74
+ cwd=source / "web",
75
+ env=environment,
76
+ )
77
+ binary.chmod(0o755)
78
+ shutil.copytree(source / "web" / "static", root / "static")
79
+ shutil.copytree(
80
+ source / "web" / "data" / "database",
81
+ root / "data" / "database",
82
+ )
83
+ filename = f"tdx-api-{UPSTREAM_SHA[:12]}-darwin-{architecture}.tar.gz"
84
+ archive = args.output / filename
85
+ _write_deterministic_archive(root, archive)
86
+ assets[architecture] = {
87
+ "filename": filename,
88
+ "sha256": _sha256(archive),
89
+ }
90
+
91
+ manifest = {
92
+ "schema_version": 1,
93
+ "api_major": 1,
94
+ "artifact_generation": ARTIFACT_GENERATION,
95
+ "upstream_url": UPSTREAM_URL,
96
+ "upstream_sha": UPSTREAM_SHA,
97
+ "patches": [
98
+ {"filename": patch.name, "sha256": _sha256(patch)} for patch in PATCH_FILES
99
+ ],
100
+ "assets": assets,
101
+ }
102
+ (args.output / "manifest.json").write_text(
103
+ json.dumps(manifest, indent=2, sort_keys=True) + "\n",
104
+ encoding="utf-8",
105
+ )
106
+
107
+
108
+ def _require_go() -> Path:
109
+ raw = shutil.which("go")
110
+ if raw is None:
111
+ raise SystemExit("Go 1.23+ is required to regenerate bundled assets")
112
+ result = _run([raw, "version"])
113
+ match = GO_VERSION_RE.search(result.stdout)
114
+ if match is None or (int(match.group(1)), int(match.group(2))) < (1, 23):
115
+ raise SystemExit(f"Go 1.23+ is required, found: {result.stdout.strip()}")
116
+ return Path(raw).resolve()
117
+
118
+
119
+ def _verify_source(source: Path) -> None:
120
+ head = _run(["git", "-C", str(source), "rev-parse", "HEAD"]).stdout.strip()
121
+ if head != UPSTREAM_SHA:
122
+ raise SystemExit(f"source HEAD is {head}, expected {UPSTREAM_SHA}")
123
+ if not (source / "web" / "go.mod").is_file():
124
+ raise SystemExit(f"source does not contain web/go.mod: {source}")
125
+ status = _run(
126
+ ["git", "-C", str(source), "status", "--short", "--untracked-files=all"]
127
+ ).stdout.strip()
128
+ if status:
129
+ raise SystemExit(f"source checkout is dirty:\n{status}")
130
+
131
+
132
+ def _apply_patches(source: Path) -> None:
133
+ for patch in PATCH_FILES:
134
+ if not patch.is_file():
135
+ raise SystemExit(f"required private patch is missing: {patch}")
136
+ _run(["git", "-C", str(source), "apply", "--check", str(patch)])
137
+ _run(["git", "-C", str(source), "apply", str(patch)])
138
+
139
+
140
+ def _write_deterministic_archive(source: Path, destination: Path) -> None:
141
+ with (
142
+ destination.open("wb") as raw,
143
+ gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed,
144
+ tarfile.open(mode="w", fileobj=compressed, format=tarfile.PAX_FORMAT) as tar,
145
+ ):
146
+ for path in sorted(source.rglob("*")):
147
+ relative = path.relative_to(source)
148
+ info = tar.gettarinfo(str(path), arcname=str(relative))
149
+ info.uid = 0
150
+ info.gid = 0
151
+ info.uname = ""
152
+ info.gname = ""
153
+ info.mtime = 0
154
+ if path.is_file():
155
+ with path.open("rb") as stream:
156
+ tar.addfile(info, stream)
157
+ else:
158
+ tar.addfile(info)
159
+
160
+
161
+ def _sha256(path: Path) -> str:
162
+ digest = hashlib.sha256()
163
+ with path.open("rb") as stream:
164
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
165
+ digest.update(chunk)
166
+ return digest.hexdigest()
167
+
168
+
169
+ def _run(
170
+ command: list[str],
171
+ *,
172
+ cwd: Path | None = None,
173
+ env: dict[str, str] | None = None,
174
+ ) -> subprocess.CompletedProcess[str]:
175
+ result = subprocess.run(
176
+ command,
177
+ cwd=cwd,
178
+ env=env,
179
+ check=False,
180
+ stdout=subprocess.PIPE,
181
+ stderr=subprocess.STDOUT,
182
+ text=True,
183
+ )
184
+ if result.returncode != 0:
185
+ rendered = " ".join(command)
186
+ raise SystemExit(
187
+ f"command failed ({result.returncode}): {rendered}\n{result.stdout}"
188
+ )
189
+ return result
190
+
191
+
192
+ if __name__ == "__main__":
193
+ main()
@@ -0,0 +1,34 @@
1
+ """Shared tdx-api client and macOS runtime."""
2
+
3
+ from pytdxfeed._version import __version__
4
+ from pytdxfeed.client import (
5
+ BASE_URL,
6
+ DEFAULT_PORT,
7
+ HOST,
8
+ TdxApiClient,
9
+ local_base_url,
10
+ validate_port,
11
+ )
12
+ from pytdxfeed.errors import (
13
+ TdxApiDeploymentError,
14
+ TdxApiError,
15
+ TdxApiResponseError,
16
+ TdxApiTransportError,
17
+ )
18
+ from pytdxfeed.runtime import ServiceInfo, ensure_service
19
+
20
+ __all__ = [
21
+ "BASE_URL",
22
+ "DEFAULT_PORT",
23
+ "HOST",
24
+ "ServiceInfo",
25
+ "TdxApiClient",
26
+ "TdxApiDeploymentError",
27
+ "TdxApiError",
28
+ "TdxApiResponseError",
29
+ "TdxApiTransportError",
30
+ "__version__",
31
+ "ensure_service",
32
+ "local_base_url",
33
+ "validate_port",
34
+ ]
@@ -0,0 +1,3 @@
1
+ """Package version shared by runtime metadata and public exports."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,27 @@
1
+ {
2
+ "api_major": 1,
3
+ "artifact_generation": 3,
4
+ "assets": {
5
+ "arm64": {
6
+ "filename": "tdx-api-ea07dccb67ae-darwin-arm64.tar.gz",
7
+ "sha256": "8b99f16639c248c3b634f613abfc9d8bd469f5428aa70b6f16ca38c700a7e8c5"
8
+ },
9
+ "x86_64": {
10
+ "filename": "tdx-api-ea07dccb67ae-darwin-x86_64.tar.gz",
11
+ "sha256": "9696b1332b826bb0d723da52cd3c558ae529ed31de6a6989f6d9e982659a4a31"
12
+ }
13
+ },
14
+ "patches": [
15
+ {
16
+ "filename": "tdx-api-ths-response-validation.patch",
17
+ "sha256": "6f27e36df68011cd60038b701b155788a7b90ad0cd7522c37144042cd6d17e20"
18
+ },
19
+ {
20
+ "filename": "tdx-api-configurable-port.patch",
21
+ "sha256": "f1c7d6824ce8f5c486d854a85ffab30ecef053e243b25b017c953b2b505e28a2"
22
+ }
23
+ ],
24
+ "schema_version": 1,
25
+ "upstream_sha": "ea07dccb67aeb92ebde851ac31503b4bf457a318",
26
+ "upstream_url": "https://github.com/oficcejo/tdx-api.git"
27
+ }