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.
- pytdxfeed-0.2.0/.gitignore +8 -0
- pytdxfeed-0.2.0/AGENTS.md +24 -0
- pytdxfeed-0.2.0/PKG-INFO +72 -0
- pytdxfeed-0.2.0/README.md +63 -0
- pytdxfeed-0.2.0/patches/tdx-api-configurable-port.patch +85 -0
- pytdxfeed-0.2.0/patches/tdx-api-ths-response-validation.patch +101 -0
- pytdxfeed-0.2.0/pyproject.toml +53 -0
- pytdxfeed-0.2.0/scripts/build_assets.py +193 -0
- pytdxfeed-0.2.0/src/pytdxfeed/__init__.py +34 -0
- pytdxfeed-0.2.0/src/pytdxfeed/_version.py +3 -0
- pytdxfeed-0.2.0/src/pytdxfeed/assets/manifest.json +27 -0
- pytdxfeed-0.2.0/src/pytdxfeed/assets/tdx-api-ea07dccb67ae-darwin-arm64.tar.gz +0 -0
- pytdxfeed-0.2.0/src/pytdxfeed/assets/tdx-api-ea07dccb67ae-darwin-x86_64.tar.gz +0 -0
- pytdxfeed-0.2.0/src/pytdxfeed/client.py +172 -0
- pytdxfeed-0.2.0/src/pytdxfeed/errors.py +17 -0
- pytdxfeed-0.2.0/src/pytdxfeed/runtime.py +648 -0
- pytdxfeed-0.2.0/tests/test_client.py +135 -0
- pytdxfeed-0.2.0/tests/test_runtime.py +172 -0
- pytdxfeed-0.2.0/uv.lock +202 -0
|
@@ -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.
|
pytdxfeed-0.2.0/PKG-INFO
ADDED
|
@@ -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,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
|
+
}
|