deeptrade-quant 0.4.0__tar.gz → 0.4.2__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.
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/PKG-INFO +1 -1
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/__init__.py +1 -1
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/cli.py +24 -3
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/db.py +24 -1
- deeptrade_quant-0.4.2/deeptrade/core/migrations/core/20260512_001_drop_legacy_tushare_cache.sql +10 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/tushare_client.py +40 -3
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/plugins_api/__init__.py +4 -0
- deeptrade_quant-0.4.2/deeptrade/plugins_api/errors.py +45 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/pyproject.toml +1 -1
- deeptrade_quant-0.4.2/tests/core/test_db.py +252 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_tushare_client.py +34 -0
- deeptrade_quant-0.4.2/tests/plugins_api/test_errors.py +77 -0
- deeptrade_quant-0.4.0/tests/core/test_db.py +0 -127
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/.gitignore +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/LICENSE +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/README.md +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/cli_config.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/cli_data.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/cli_plugin.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/__init__.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/config.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/config_migrations.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/dep_installer.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/github_fetch.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/llm_client.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/llm_manager.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/logging_config.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/migrations/__init__.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/migrations/core/20260509_001_init.sql +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/migrations/core/__init__.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/paths.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/plugin_manager.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/plugin_source.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/registry.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/run_status.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/secrets.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/plugins_api/base.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/plugins_api/events.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/plugins_api/llm.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/plugins_api/metadata.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/theme.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/__init__.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/cli/__init__.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/cli/test_config_cmd.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/cli/test_plugin_cmd.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/cli/test_routing.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/conftest.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/__init__.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_config.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_config_migrations.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_github_fetch.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_llm_client.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_llm_manager.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_paths.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_plugin_dependencies.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_plugin_install.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_plugin_source.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_plugin_upgrade.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_registry.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_secrets.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_tushare_classifier.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/core/test_tushare_retry_r1.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/plugins_api/__init__.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/plugins_api/test_protocol.py +0 -0
- {deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deeptrade-quant
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: LLM-driven A-share (Shanghai/Shenzhen main board) stock screening CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/ty19880929/deeptrade
|
|
6
6
|
Project-URL: Repository, https://github.com/ty19880929/deeptrade
|
|
@@ -126,7 +126,20 @@ def _build_plugin_command(plugin_id: str) -> click.Command | None:
|
|
|
126
126
|
if not hasattr(plugin, "dispatch"):
|
|
127
127
|
typer.echo(f"✘ plugin {plugin_id!r} does not implement dispatch()")
|
|
128
128
|
raise typer.Exit(2)
|
|
129
|
-
|
|
129
|
+
try:
|
|
130
|
+
rc = plugin.dispatch(list(ctx.args))
|
|
131
|
+
except (SystemExit, KeyboardInterrupt):
|
|
132
|
+
# Exit codes / Ctrl-C must propagate unaltered.
|
|
133
|
+
raise
|
|
134
|
+
except BaseException as e: # noqa: BLE001 — final safety net
|
|
135
|
+
# Plugins are encouraged to install their own dispatch-tail handler
|
|
136
|
+
# (see plugins_api.render_exception). This catch only fires when a
|
|
137
|
+
# plugin lets an exception escape — we still want DEEPTRADE_DEBUG=1
|
|
138
|
+
# to surface the traceback rather than a bare crash.
|
|
139
|
+
from deeptrade.plugins_api import render_exception
|
|
140
|
+
|
|
141
|
+
sys.stderr.write(render_exception(e) + "\n")
|
|
142
|
+
raise typer.Exit(1) from e
|
|
130
143
|
raise typer.Exit(rc or 0)
|
|
131
144
|
|
|
132
145
|
return _dispatch
|
|
@@ -181,10 +194,16 @@ def init(
|
|
|
181
194
|
paths.ensure_layout()
|
|
182
195
|
db_file = paths.db_path()
|
|
183
196
|
fresh = not db_file.exists()
|
|
184
|
-
|
|
197
|
+
# auto_migrate=False so we can collect & print the precise list of versions
|
|
198
|
+
# newly applied below; the auto-migrate path swallows that information by
|
|
199
|
+
# design (it runs unconditionally on every Database open).
|
|
200
|
+
db = Database(db_file, auto_migrate=False)
|
|
185
201
|
try:
|
|
202
|
+
applied = apply_core_migrations(db)
|
|
186
203
|
if fresh:
|
|
187
204
|
typer.echo(f"✔ Database created: {db_file}")
|
|
205
|
+
for v in applied:
|
|
206
|
+
typer.echo(f"✔ Schema applied: {v}")
|
|
188
207
|
finally:
|
|
189
208
|
db.close()
|
|
190
209
|
|
|
@@ -219,7 +238,9 @@ def db_init() -> None:
|
|
|
219
238
|
paths.ensure_layout()
|
|
220
239
|
db_file = paths.db_path()
|
|
221
240
|
fresh = not db_file.exists()
|
|
222
|
-
|
|
241
|
+
# auto_migrate=False so this command reports which versions it applied;
|
|
242
|
+
# the Database auto-migrate path returns nothing.
|
|
243
|
+
db = Database(db_file, auto_migrate=False)
|
|
223
244
|
try:
|
|
224
245
|
applied = apply_core_migrations(db)
|
|
225
246
|
if fresh:
|
|
@@ -6,6 +6,7 @@ held by AppContext; writes are serialized on the runner main thread.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import os
|
|
9
10
|
import re
|
|
10
11
|
import threading
|
|
11
12
|
from collections.abc import Iterator
|
|
@@ -20,6 +21,20 @@ from deeptrade.core import paths
|
|
|
20
21
|
|
|
21
22
|
_MIGRATION_FILENAME_RE = re.compile(r"^(\d{8}_\d{3,})_.+\.sql$")
|
|
22
23
|
|
|
24
|
+
# Escape hatch: set DEEPTRADE_SKIP_AUTO_MIGRATE=1 to bypass the auto-migrate
|
|
25
|
+
# step in Database.__init__. Intended for recovery (e.g. a corrupt schema
|
|
26
|
+
# blocking every CLI command); not part of the documented main flow.
|
|
27
|
+
_SKIP_AUTO_MIGRATE_ENV = "DEEPTRADE_SKIP_AUTO_MIGRATE"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _skip_auto_migrate() -> bool:
|
|
31
|
+
return os.environ.get(_SKIP_AUTO_MIGRATE_ENV, "").strip().lower() in {
|
|
32
|
+
"1",
|
|
33
|
+
"true",
|
|
34
|
+
"yes",
|
|
35
|
+
"on",
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
|
|
24
39
|
class Database:
|
|
25
40
|
"""Thin wrapper around a single DuckDB connection.
|
|
@@ -29,7 +44,7 @@ class Database:
|
|
|
29
44
|
consumed by the main thread (DESIGN §13.3).
|
|
30
45
|
"""
|
|
31
46
|
|
|
32
|
-
def __init__(self, db_file: Path | None = None) -> None:
|
|
47
|
+
def __init__(self, db_file: Path | None = None, *, auto_migrate: bool = True) -> None:
|
|
33
48
|
self._path = db_file or paths.db_path()
|
|
34
49
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
50
|
self._conn: duckdb.DuckDBPyConnection = duckdb.connect(str(self._path))
|
|
@@ -39,6 +54,14 @@ class Database:
|
|
|
39
54
|
self._write_lock = threading.RLock()
|
|
40
55
|
self._tx_depth = 0 # for reentrant transaction(); only outermost BEGIN/COMMIT
|
|
41
56
|
|
|
57
|
+
# Auto-migrate: any CLI / SDK code path that opens the DB pulls schema
|
|
58
|
+
# forward so wheel upgrades don't strand stale rows for the new
|
|
59
|
+
# readers (see v0.4.1 tushare_cache_blob format change). The CLI's own
|
|
60
|
+
# `init` / `db init` / `db upgrade` commands opt out so they can print
|
|
61
|
+
# the precise list of versions newly applied.
|
|
62
|
+
if auto_migrate and not _skip_auto_migrate():
|
|
63
|
+
apply_core_migrations(self)
|
|
64
|
+
|
|
42
65
|
@property
|
|
43
66
|
def path(self) -> Path:
|
|
44
67
|
return self._path
|
deeptrade_quant-0.4.2/deeptrade/core/migrations/core/20260512_001_drop_legacy_tushare_cache.sql
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- v0.4.1 — drop legacy tushare_cache_blob entries.
|
|
2
|
+
--
|
|
3
|
+
-- Pre-v0.4.1 payloads were stored as a bare JSON array, then read back via
|
|
4
|
+
-- pd.read_json which triggered a pandas FutureWarning on string columns whose
|
|
5
|
+
-- names matched its date heuristic (trade_date, cal_date, ...). The cache
|
|
6
|
+
-- read/write path now wraps payloads as {version:1, schema:{...}, data:[...]}
|
|
7
|
+
-- and restores dtypes explicitly. Old rows are incompatible with that reader,
|
|
8
|
+
-- so wipe the table; TushareClient._ensure_cache_table re-creates it lazily on
|
|
9
|
+
-- the next write.
|
|
10
|
+
DROP TABLE IF EXISTS tushare_cache_blob;
|
|
@@ -19,7 +19,6 @@ Cache class buckets:
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import hashlib
|
|
22
|
-
import io
|
|
23
22
|
import json
|
|
24
23
|
import logging
|
|
25
24
|
import threading
|
|
@@ -896,6 +895,15 @@ class TushareClient:
|
|
|
896
895
|
")"
|
|
897
896
|
)
|
|
898
897
|
|
|
898
|
+
# Payload wrapper format (v0.4.1+):
|
|
899
|
+
# {"version": 1, "schema": {col: dtype_str}, "data": [...records...]}
|
|
900
|
+
# Bypasses pd.read_json's date-column heuristic (which warns on string
|
|
901
|
+
# columns named like dates — trade_date / cal_date / ann_date) and
|
|
902
|
+
# restores dtypes explicitly from the recorded schema. Pre-v0.4.1 rows
|
|
903
|
+
# were a bare records array; those are wiped by core migration
|
|
904
|
+
# 20260512_001_drop_legacy_tushare_cache.sql, so no legacy branch here.
|
|
905
|
+
_CACHE_PAYLOAD_VERSION = 1
|
|
906
|
+
|
|
899
907
|
def _write_cached(
|
|
900
908
|
self,
|
|
901
909
|
api_name: str,
|
|
@@ -906,7 +914,14 @@ class TushareClient:
|
|
|
906
914
|
self._ensure_cache_table()
|
|
907
915
|
body = json.dumps(params, sort_keys=True, default=str)
|
|
908
916
|
h = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
|
909
|
-
|
|
917
|
+
records_json = df.to_json(orient="records", date_format="iso")
|
|
918
|
+
payload = json.dumps(
|
|
919
|
+
{
|
|
920
|
+
"version": self._CACHE_PAYLOAD_VERSION,
|
|
921
|
+
"schema": {col: str(dt) for col, dt in df.dtypes.items()},
|
|
922
|
+
"data": json.loads(records_json) if records_json else [],
|
|
923
|
+
}
|
|
924
|
+
)
|
|
910
925
|
with self._db.transaction():
|
|
911
926
|
self._db.execute(
|
|
912
927
|
"DELETE FROM tushare_cache_blob "
|
|
@@ -937,12 +952,34 @@ class TushareClient:
|
|
|
937
952
|
)
|
|
938
953
|
if row is None:
|
|
939
954
|
return pd.DataFrame()
|
|
940
|
-
|
|
955
|
+
wrapper = json.loads(row[0])
|
|
956
|
+
df = self._restore_cached_frame(wrapper)
|
|
941
957
|
if fields:
|
|
942
958
|
cols = [c.strip() for c in fields.split(",") if c.strip() in df.columns]
|
|
943
959
|
df = df[cols]
|
|
944
960
|
return df
|
|
945
961
|
|
|
962
|
+
@classmethod
|
|
963
|
+
def _restore_cached_frame(cls, wrapper: dict[str, Any]) -> pd.DataFrame:
|
|
964
|
+
schema: dict[str, str] = wrapper["schema"]
|
|
965
|
+
data: list[dict[str, Any]] = wrapper["data"]
|
|
966
|
+
df = pd.DataFrame.from_records(data, columns=list(schema.keys()))
|
|
967
|
+
for col, dtype_str in schema.items():
|
|
968
|
+
if col not in df.columns:
|
|
969
|
+
continue
|
|
970
|
+
if dtype_str.startswith("datetime"):
|
|
971
|
+
df[col] = pd.to_datetime(df[col], errors="coerce")
|
|
972
|
+
elif dtype_str == "object":
|
|
973
|
+
continue
|
|
974
|
+
else:
|
|
975
|
+
try:
|
|
976
|
+
df[col] = df[col].astype(dtype_str)
|
|
977
|
+
except (TypeError, ValueError):
|
|
978
|
+
# Best-effort: if a numeric/bool column can't be coerced
|
|
979
|
+
# back (e.g. all-null), leave the inferred dtype.
|
|
980
|
+
pass
|
|
981
|
+
return df
|
|
982
|
+
|
|
946
983
|
|
|
947
984
|
# ---------------------------------------------------------------------------
|
|
948
985
|
# Fallback predicate (DESIGN §13.2 + S4)
|
|
@@ -6,11 +6,13 @@ Stable surface (api_version = "1"):
|
|
|
6
6
|
- Plugin (Protocol), PluginContext — every plugin's contract: metadata + validate_static + dispatch
|
|
7
7
|
- PluginMetadata, TableSpec, MigrationSpec, PluginPermissions, ...
|
|
8
8
|
- StageProfile — LLM 调参档;插件持有 preset → stage 映射表,自行解析
|
|
9
|
+
- render_exception — uniform DEEPTRADE_DEBUG-aware error formatter for dispatch tails
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
from __future__ import annotations
|
|
12
13
|
|
|
13
14
|
from deeptrade.plugins_api.base import Plugin, PluginContext
|
|
15
|
+
from deeptrade.plugins_api.errors import debug_enabled, render_exception
|
|
14
16
|
from deeptrade.plugins_api.llm import StageProfile
|
|
15
17
|
from deeptrade.plugins_api.metadata import (
|
|
16
18
|
MigrationSpec,
|
|
@@ -29,4 +31,6 @@ __all__ = [
|
|
|
29
31
|
"StageProfile",
|
|
30
32
|
"TableSpec",
|
|
31
33
|
"TushareApiPermissions",
|
|
34
|
+
"debug_enabled",
|
|
35
|
+
"render_exception",
|
|
32
36
|
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Error rendering helpers for plugin dispatch.
|
|
2
|
+
|
|
3
|
+
Plugins commonly install a generic ``except Exception`` at the top of their
|
|
4
|
+
``dispatch`` to translate any uncaught error into a non-zero exit code while
|
|
5
|
+
keeping the user-facing output short. The downside is the loss of the
|
|
6
|
+
traceback when something unexpected goes wrong.
|
|
7
|
+
|
|
8
|
+
``render_exception`` centralizes that one-liner/traceback decision behind a
|
|
9
|
+
single environment variable so users can opt into a full stack without
|
|
10
|
+
plugins inventing their own conventions:
|
|
11
|
+
|
|
12
|
+
DEEPTRADE_DEBUG=1 → traceback (chained causes included)
|
|
13
|
+
unset / "0" / "" → one-line "{glyph} {ExcType}: {msg}"
|
|
14
|
+
|
|
15
|
+
Both the framework (``deeptrade/cli.py::_dispatch``) and plugin dispatch
|
|
16
|
+
tails should use this helper so the toggle is honored uniformly.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import traceback
|
|
23
|
+
|
|
24
|
+
_DEBUG_ENV = "DEEPTRADE_DEBUG"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def debug_enabled() -> bool:
|
|
28
|
+
"""True when ``DEEPTRADE_DEBUG`` is set to a truthy value."""
|
|
29
|
+
return os.environ.get(_DEBUG_ENV, "").strip().lower() in {"1", "true", "yes", "on"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def render_exception(exc: BaseException, *, header_glyph: str = "✘") -> str:
|
|
33
|
+
"""Format ``exc`` for stderr.
|
|
34
|
+
|
|
35
|
+
With ``DEEPTRADE_DEBUG=1`` returns the full ``traceback.format_exception``
|
|
36
|
+
output (which already includes chained causes and exception groups)
|
|
37
|
+
prefixed by ``header_glyph``. Otherwise returns the one-liner
|
|
38
|
+
``"{header_glyph} {ExcType}: {message}"``.
|
|
39
|
+
|
|
40
|
+
The output has no trailing newline; callers append their own.
|
|
41
|
+
"""
|
|
42
|
+
if debug_enabled():
|
|
43
|
+
body = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
44
|
+
return f"{header_glyph} {body.rstrip()}"
|
|
45
|
+
return f"{header_glyph} {type(exc).__name__}: {exc}"
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Core schema invariants — framework-only tables (Plan A pure isolation).
|
|
2
|
+
|
|
3
|
+
After the v0.5 reshape the framework owns ONLY:
|
|
4
|
+
app_config, secret_store, schema_migrations,
|
|
5
|
+
plugins, plugin_tables, plugin_schema_migrations,
|
|
6
|
+
llm_calls, tushare_sync_state, tushare_calls
|
|
7
|
+
|
|
8
|
+
All business / strategy data is plugin-owned.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from typer.testing import CliRunner
|
|
17
|
+
|
|
18
|
+
from deeptrade.cli import app
|
|
19
|
+
from deeptrade.core.db import Database, apply_core_migrations
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def fresh_db(tmp_path: Path) -> Database:
|
|
24
|
+
# auto_migrate=False keeps the "fresh" semantics tests depend on: each
|
|
25
|
+
# test exercises apply_core_migrations explicitly to assert what it does.
|
|
26
|
+
return Database(tmp_path / "test.duckdb", auto_migrate=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --- init creates DB + dirs ----------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_init_creates_db_file_and_dirs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
33
|
+
monkeypatch.setenv("DEEPTRADE_HOME", str(tmp_path))
|
|
34
|
+
runner = CliRunner()
|
|
35
|
+
result = runner.invoke(app, ["init", "--no-prompts"])
|
|
36
|
+
assert result.exit_code == 0, result.stdout
|
|
37
|
+
assert (tmp_path / "deeptrade.duckdb").is_file()
|
|
38
|
+
assert (tmp_path / "logs").is_dir()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_init_is_idempotent(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
42
|
+
monkeypatch.setenv("DEEPTRADE_HOME", str(tmp_path))
|
|
43
|
+
runner = CliRunner()
|
|
44
|
+
runner.invoke(app, ["init", "--no-prompts"])
|
|
45
|
+
result = runner.invoke(app, ["init", "--no-prompts"])
|
|
46
|
+
assert result.exit_code == 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# --- migrations record version --------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_apply_core_migrations_records_version(fresh_db: Database) -> None:
|
|
53
|
+
"""Framework owns no business tables."""
|
|
54
|
+
applied = apply_core_migrations(fresh_db)
|
|
55
|
+
assert applied == ["20260509_001", "20260512_001"]
|
|
56
|
+
rows = fresh_db.fetchall("SELECT version FROM schema_migrations ORDER BY version")
|
|
57
|
+
assert tuple(rows) == (("20260509_001",), ("20260512_001",))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_apply_core_migrations_skips_applied_versions(fresh_db: Database) -> None:
|
|
61
|
+
apply_core_migrations(fresh_db)
|
|
62
|
+
second = apply_core_migrations(fresh_db)
|
|
63
|
+
assert second == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --- framework tables exist; business tables do NOT -----------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_framework_owns_only_minimal_table_set(fresh_db: Database) -> None:
|
|
70
|
+
apply_core_migrations(fresh_db)
|
|
71
|
+
tables = {
|
|
72
|
+
r[0]
|
|
73
|
+
for r in fresh_db.fetchall(
|
|
74
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
expected = {
|
|
78
|
+
"app_config",
|
|
79
|
+
"secret_store",
|
|
80
|
+
"schema_migrations",
|
|
81
|
+
"plugins",
|
|
82
|
+
"plugin_tables",
|
|
83
|
+
"plugin_schema_migrations",
|
|
84
|
+
"llm_calls",
|
|
85
|
+
"tushare_sync_state",
|
|
86
|
+
"tushare_calls",
|
|
87
|
+
}
|
|
88
|
+
assert tables == expected, f"unexpected drift: {tables - expected} missing: {expected - tables}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- tushare_sync_state has plugin_id in PK (Plan A pure isolation) -------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_tushare_sync_state_has_plugin_id_column_and_pk(fresh_db: Database) -> None:
|
|
95
|
+
apply_core_migrations(fresh_db)
|
|
96
|
+
cols = [
|
|
97
|
+
r[0]
|
|
98
|
+
for r in fresh_db.fetchall(
|
|
99
|
+
"SELECT column_name FROM information_schema.columns "
|
|
100
|
+
"WHERE table_name='tushare_sync_state' ORDER BY ordinal_position"
|
|
101
|
+
)
|
|
102
|
+
]
|
|
103
|
+
# plugin_id is the first PK column
|
|
104
|
+
assert cols[0] == "plugin_id"
|
|
105
|
+
assert "data_completeness" in cols
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_tushare_sync_state_default_data_completeness(fresh_db: Database) -> None:
|
|
109
|
+
apply_core_migrations(fresh_db)
|
|
110
|
+
fresh_db.execute(
|
|
111
|
+
"INSERT INTO tushare_sync_state(plugin_id, api_name, trade_date, status) "
|
|
112
|
+
"VALUES (?, ?, ?, ?)",
|
|
113
|
+
("test-plugin", "stock_basic", "*", "ok"),
|
|
114
|
+
)
|
|
115
|
+
row = fresh_db.fetchone(
|
|
116
|
+
"SELECT data_completeness FROM tushare_sync_state WHERE api_name='stock_basic'"
|
|
117
|
+
)
|
|
118
|
+
assert row is not None and row[0] == "final"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_tushare_calls_has_plugin_id_column(fresh_db: Database) -> None:
|
|
122
|
+
apply_core_migrations(fresh_db)
|
|
123
|
+
cols = {
|
|
124
|
+
r[0]
|
|
125
|
+
for r in fresh_db.fetchall(
|
|
126
|
+
"SELECT column_name FROM information_schema.columns WHERE table_name='tushare_calls'"
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
assert "plugin_id" in cols
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# --- Database.__init__ auto-migrate (v0.4.2) -----------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_database_init_runs_pending_migrations(tmp_path: Path) -> None:
|
|
136
|
+
"""A fresh Database() with default auto_migrate=True applies every core
|
|
137
|
+
migration on first open — no separate `db upgrade` call needed."""
|
|
138
|
+
db = Database(tmp_path / "auto.duckdb")
|
|
139
|
+
try:
|
|
140
|
+
rows = db.fetchall("SELECT version FROM schema_migrations ORDER BY version")
|
|
141
|
+
assert tuple(rows) == (("20260509_001",), ("20260512_001",))
|
|
142
|
+
finally:
|
|
143
|
+
db.close()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_database_init_auto_migrate_false_skips(tmp_path: Path) -> None:
|
|
147
|
+
"""auto_migrate=False is the documented opt-out for the CLI's
|
|
148
|
+
`db init` / `db upgrade` commands that want to collect the applied
|
|
149
|
+
list themselves."""
|
|
150
|
+
db = Database(tmp_path / "manual.duckdb", auto_migrate=False)
|
|
151
|
+
try:
|
|
152
|
+
rows = db.fetchall(
|
|
153
|
+
"SELECT table_name FROM information_schema.tables "
|
|
154
|
+
"WHERE table_schema='main' AND table_name='schema_migrations'"
|
|
155
|
+
)
|
|
156
|
+
assert rows == []
|
|
157
|
+
finally:
|
|
158
|
+
db.close()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_database_init_env_var_skips_auto_migrate(
|
|
162
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
163
|
+
) -> None:
|
|
164
|
+
"""DEEPTRADE_SKIP_AUTO_MIGRATE=1 is the recovery escape hatch."""
|
|
165
|
+
monkeypatch.setenv("DEEPTRADE_SKIP_AUTO_MIGRATE", "1")
|
|
166
|
+
db = Database(tmp_path / "skip.duckdb")
|
|
167
|
+
try:
|
|
168
|
+
rows = db.fetchall(
|
|
169
|
+
"SELECT table_name FROM information_schema.tables "
|
|
170
|
+
"WHERE table_schema='main' AND table_name='schema_migrations'"
|
|
171
|
+
)
|
|
172
|
+
assert rows == []
|
|
173
|
+
finally:
|
|
174
|
+
db.close()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_database_init_migration_failure_propagates(
|
|
178
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
179
|
+
) -> None:
|
|
180
|
+
"""A failing migration must bubble out of Database() as a hard error so
|
|
181
|
+
callers can't proceed against a half-migrated schema."""
|
|
182
|
+
from deeptrade.core import db as db_module
|
|
183
|
+
|
|
184
|
+
boom = RuntimeError("synthetic migration failure")
|
|
185
|
+
|
|
186
|
+
def _failing(_db: Database) -> list[str]:
|
|
187
|
+
raise boom
|
|
188
|
+
|
|
189
|
+
monkeypatch.setattr(db_module, "apply_core_migrations", _failing)
|
|
190
|
+
|
|
191
|
+
with pytest.raises(RuntimeError, match="synthetic migration failure"):
|
|
192
|
+
Database(tmp_path / "broken.duckdb")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_legacy_tushare_cache_wiped_on_first_open(tmp_path: Path) -> None:
|
|
196
|
+
"""v0.4.1 introduced a wrapped tushare cache payload format and a
|
|
197
|
+
migration that drops the legacy table. Auto-migrate must apply that
|
|
198
|
+
migration on first open so a pre-0.4.1 DB doesn't poison the v0.4.1+
|
|
199
|
+
reader. Regression-locks the TypeError reported against
|
|
200
|
+
`deeptrade limit-up-board lgb train`.
|
|
201
|
+
"""
|
|
202
|
+
db_file = tmp_path / "legacy.duckdb"
|
|
203
|
+
|
|
204
|
+
# Stage 1: simulate a pre-0.4.1 DB — only the very first migration is on
|
|
205
|
+
# record, and the legacy cache table has a bare-array payload row.
|
|
206
|
+
db = Database(db_file, auto_migrate=False)
|
|
207
|
+
try:
|
|
208
|
+
# Apply only the v0.4.0 migration (init schema) by name; intentionally
|
|
209
|
+
# leave the v0.4.1 drop migration unrecorded.
|
|
210
|
+
from deeptrade.core.db import _list_core_migrations
|
|
211
|
+
|
|
212
|
+
for version, sql_text in _list_core_migrations():
|
|
213
|
+
if version != "20260509_001":
|
|
214
|
+
continue
|
|
215
|
+
with db.transaction():
|
|
216
|
+
db.execute(sql_text)
|
|
217
|
+
db.execute(
|
|
218
|
+
"INSERT INTO schema_migrations(version) VALUES (?)",
|
|
219
|
+
(version,),
|
|
220
|
+
)
|
|
221
|
+
db.execute(
|
|
222
|
+
"CREATE TABLE tushare_cache_blob ("
|
|
223
|
+
" plugin_id VARCHAR NOT NULL,"
|
|
224
|
+
" api_name VARCHAR NOT NULL,"
|
|
225
|
+
" trade_date VARCHAR NOT NULL,"
|
|
226
|
+
" params_hash VARCHAR NOT NULL,"
|
|
227
|
+
" payload_json VARCHAR NOT NULL,"
|
|
228
|
+
" cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
|
|
229
|
+
" PRIMARY KEY (plugin_id, api_name, trade_date, params_hash)"
|
|
230
|
+
")"
|
|
231
|
+
)
|
|
232
|
+
db.execute(
|
|
233
|
+
"INSERT INTO tushare_cache_blob(plugin_id, api_name, trade_date, "
|
|
234
|
+
"params_hash, payload_json) VALUES (?, ?, ?, ?, ?)",
|
|
235
|
+
("legacy-plugin", "trade_cal", "*", "0" * 64, '[{"cal_date":"20260101"}]'),
|
|
236
|
+
)
|
|
237
|
+
finally:
|
|
238
|
+
db.close()
|
|
239
|
+
|
|
240
|
+
# Stage 2: re-open with auto_migrate=True → the drop migration must run
|
|
241
|
+
# and the legacy table must be gone.
|
|
242
|
+
db = Database(db_file)
|
|
243
|
+
try:
|
|
244
|
+
rows = db.fetchall(
|
|
245
|
+
"SELECT table_name FROM information_schema.tables "
|
|
246
|
+
"WHERE table_schema='main' AND table_name='tushare_cache_blob'"
|
|
247
|
+
)
|
|
248
|
+
assert rows == [], "legacy tushare_cache_blob should be dropped by auto-migrate"
|
|
249
|
+
applied = {r[0] for r in db.fetchall("SELECT version FROM schema_migrations")}
|
|
250
|
+
assert "20260512_001" in applied
|
|
251
|
+
finally:
|
|
252
|
+
db.close()
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import warnings
|
|
8
9
|
from datetime import datetime, timedelta
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
@@ -452,3 +453,36 @@ def test_cache_hit_requires_payload_present(
|
|
|
452
453
|
client.call("limit_list_d", trade_date="20260427")
|
|
453
454
|
# Should hit transport (because no payload despite state=ok)
|
|
454
455
|
assert len(transport.calls) == 1
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
# Cache payload round-trip — dtypes preserved, no pandas FutureWarning
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def test_cache_payload_round_trip_preserves_dtypes(client: TushareClient) -> None:
|
|
464
|
+
"""v0.4.1 wrapper format restores dtypes explicitly so plugins don't see
|
|
465
|
+
pandas' read_json date-heuristic warnings on tushare-style date columns."""
|
|
466
|
+
df = pd.DataFrame(
|
|
467
|
+
{
|
|
468
|
+
"trade_date": ["20260427", "20260428"], # YYYYMMDD strings — triggers heuristic
|
|
469
|
+
"ts_code": ["000001.SZ", "000002.SZ"],
|
|
470
|
+
"close": [12.5, 14.7],
|
|
471
|
+
"vol": [1000, 2000],
|
|
472
|
+
"is_st": [True, False],
|
|
473
|
+
"ann_dt": pd.to_datetime(["2026-04-27", "2026-04-28"]), # real datetime64[ns]
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
api, td, params = "limit_list_d", "20260427", {"trade_date": "20260427"}
|
|
477
|
+
|
|
478
|
+
client._write_cached(api, td, params, df) # noqa: SLF001 — exercising internal contract
|
|
479
|
+
|
|
480
|
+
with warnings.catch_warnings():
|
|
481
|
+
warnings.simplefilter("error", FutureWarning)
|
|
482
|
+
out = client._read_cached(api, td, params, fields=None) # noqa: SLF001
|
|
483
|
+
|
|
484
|
+
assert list(out.columns) == list(df.columns)
|
|
485
|
+
for col in df.columns:
|
|
486
|
+
assert out[col].dtype == df[col].dtype, f"dtype drift on {col}: {out[col].dtype}"
|
|
487
|
+
assert out["trade_date"].tolist() == ["20260427", "20260428"]
|
|
488
|
+
assert (out["ann_dt"] == df["ann_dt"]).all()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Tests for ``deeptrade.plugins_api.errors`` — DEEPTRADE_DEBUG-aware
|
|
2
|
+
exception rendering for plugin dispatch tails."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from deeptrade.plugins_api import debug_enabled, render_exception
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _make_chained_error() -> Exception:
|
|
12
|
+
"""Build an exception with both ``__traceback__`` and ``__cause__`` set,
|
|
13
|
+
matching what a real `try/except ... raise X from e` would yield."""
|
|
14
|
+
try:
|
|
15
|
+
try:
|
|
16
|
+
raise ValueError("inner cause")
|
|
17
|
+
except ValueError as inner:
|
|
18
|
+
raise RuntimeError("outer failure") from inner
|
|
19
|
+
except RuntimeError as e:
|
|
20
|
+
return e
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --- default mode (no DEBUG) ---------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_render_exception_one_liner_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
27
|
+
monkeypatch.delenv("DEEPTRADE_DEBUG", raising=False)
|
|
28
|
+
try:
|
|
29
|
+
raise TypeError("list indices must be integers or slices, not str")
|
|
30
|
+
except TypeError as e:
|
|
31
|
+
out = render_exception(e)
|
|
32
|
+
assert out == "✘ TypeError: list indices must be integers or slices, not str"
|
|
33
|
+
assert "\n" not in out, "default mode must be a single line"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_render_exception_custom_glyph(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
37
|
+
monkeypatch.delenv("DEEPTRADE_DEBUG", raising=False)
|
|
38
|
+
e = ValueError("bad input")
|
|
39
|
+
out = render_exception(e, header_glyph="!!")
|
|
40
|
+
assert out == "!! ValueError: bad input"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --- DEBUG mode ----------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize("truthy", ["1", "true", "yes", "on", "TRUE", "On"])
|
|
47
|
+
def test_render_exception_traceback_when_debug(
|
|
48
|
+
monkeypatch: pytest.MonkeyPatch, truthy: str
|
|
49
|
+
) -> None:
|
|
50
|
+
monkeypatch.setenv("DEEPTRADE_DEBUG", truthy)
|
|
51
|
+
e = _make_chained_error()
|
|
52
|
+
out = render_exception(e)
|
|
53
|
+
# Header glyph still present
|
|
54
|
+
assert out.startswith("✘ ")
|
|
55
|
+
# Outer exception + message present
|
|
56
|
+
assert "RuntimeError: outer failure" in out
|
|
57
|
+
# Chained cause surfaced (this is the user-facing payoff over the one-liner)
|
|
58
|
+
assert "ValueError: inner cause" in out
|
|
59
|
+
# Traceback frames present (file + lineno)
|
|
60
|
+
assert "Traceback" in out
|
|
61
|
+
# No trailing newline — caller appends its own
|
|
62
|
+
assert not out.endswith("\n")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_render_exception_falsy_env_still_one_liner(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
66
|
+
monkeypatch.setenv("DEEPTRADE_DEBUG", "0")
|
|
67
|
+
e = RuntimeError("x")
|
|
68
|
+
assert render_exception(e) == "✘ RuntimeError: x"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_debug_enabled_reflects_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
72
|
+
monkeypatch.delenv("DEEPTRADE_DEBUG", raising=False)
|
|
73
|
+
assert debug_enabled() is False
|
|
74
|
+
monkeypatch.setenv("DEEPTRADE_DEBUG", "1")
|
|
75
|
+
assert debug_enabled() is True
|
|
76
|
+
monkeypatch.setenv("DEEPTRADE_DEBUG", "0")
|
|
77
|
+
assert debug_enabled() is False
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
"""Core schema invariants — framework-only tables (Plan A pure isolation).
|
|
2
|
-
|
|
3
|
-
After the v0.5 reshape the framework owns ONLY:
|
|
4
|
-
app_config, secret_store, schema_migrations,
|
|
5
|
-
plugins, plugin_tables, plugin_schema_migrations,
|
|
6
|
-
llm_calls, tushare_sync_state, tushare_calls
|
|
7
|
-
|
|
8
|
-
All business / strategy data is plugin-owned.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
import pytest
|
|
16
|
-
from typer.testing import CliRunner
|
|
17
|
-
|
|
18
|
-
from deeptrade.cli import app
|
|
19
|
-
from deeptrade.core.db import Database, apply_core_migrations
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@pytest.fixture
|
|
23
|
-
def fresh_db(tmp_path: Path) -> Database:
|
|
24
|
-
return Database(tmp_path / "test.duckdb")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# --- init creates DB + dirs ----------------------------------------------
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_init_creates_db_file_and_dirs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
31
|
-
monkeypatch.setenv("DEEPTRADE_HOME", str(tmp_path))
|
|
32
|
-
runner = CliRunner()
|
|
33
|
-
result = runner.invoke(app, ["init", "--no-prompts"])
|
|
34
|
-
assert result.exit_code == 0, result.stdout
|
|
35
|
-
assert (tmp_path / "deeptrade.duckdb").is_file()
|
|
36
|
-
assert (tmp_path / "logs").is_dir()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_init_is_idempotent(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
40
|
-
monkeypatch.setenv("DEEPTRADE_HOME", str(tmp_path))
|
|
41
|
-
runner = CliRunner()
|
|
42
|
-
runner.invoke(app, ["init", "--no-prompts"])
|
|
43
|
-
result = runner.invoke(app, ["init", "--no-prompts"])
|
|
44
|
-
assert result.exit_code == 0
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# --- migrations record version --------------------------------------------
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_apply_core_migrations_records_version(fresh_db: Database) -> None:
|
|
51
|
-
"""Framework owns no business tables."""
|
|
52
|
-
applied = apply_core_migrations(fresh_db)
|
|
53
|
-
assert applied == ["20260509_001"]
|
|
54
|
-
rows = fresh_db.fetchall("SELECT version FROM schema_migrations ORDER BY version")
|
|
55
|
-
assert tuple(rows) == (("20260509_001",),)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def test_apply_core_migrations_skips_applied_versions(fresh_db: Database) -> None:
|
|
59
|
-
apply_core_migrations(fresh_db)
|
|
60
|
-
second = apply_core_migrations(fresh_db)
|
|
61
|
-
assert second == []
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# --- framework tables exist; business tables do NOT -----------------------
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def test_framework_owns_only_minimal_table_set(fresh_db: Database) -> None:
|
|
68
|
-
apply_core_migrations(fresh_db)
|
|
69
|
-
tables = {
|
|
70
|
-
r[0]
|
|
71
|
-
for r in fresh_db.fetchall(
|
|
72
|
-
"SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
expected = {
|
|
76
|
-
"app_config",
|
|
77
|
-
"secret_store",
|
|
78
|
-
"schema_migrations",
|
|
79
|
-
"plugins",
|
|
80
|
-
"plugin_tables",
|
|
81
|
-
"plugin_schema_migrations",
|
|
82
|
-
"llm_calls",
|
|
83
|
-
"tushare_sync_state",
|
|
84
|
-
"tushare_calls",
|
|
85
|
-
}
|
|
86
|
-
assert tables == expected, f"unexpected drift: {tables - expected} missing: {expected - tables}"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# --- tushare_sync_state has plugin_id in PK (Plan A pure isolation) -------
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def test_tushare_sync_state_has_plugin_id_column_and_pk(fresh_db: Database) -> None:
|
|
93
|
-
apply_core_migrations(fresh_db)
|
|
94
|
-
cols = [
|
|
95
|
-
r[0]
|
|
96
|
-
for r in fresh_db.fetchall(
|
|
97
|
-
"SELECT column_name FROM information_schema.columns "
|
|
98
|
-
"WHERE table_name='tushare_sync_state' ORDER BY ordinal_position"
|
|
99
|
-
)
|
|
100
|
-
]
|
|
101
|
-
# plugin_id is the first PK column
|
|
102
|
-
assert cols[0] == "plugin_id"
|
|
103
|
-
assert "data_completeness" in cols
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_tushare_sync_state_default_data_completeness(fresh_db: Database) -> None:
|
|
107
|
-
apply_core_migrations(fresh_db)
|
|
108
|
-
fresh_db.execute(
|
|
109
|
-
"INSERT INTO tushare_sync_state(plugin_id, api_name, trade_date, status) "
|
|
110
|
-
"VALUES (?, ?, ?, ?)",
|
|
111
|
-
("test-plugin", "stock_basic", "*", "ok"),
|
|
112
|
-
)
|
|
113
|
-
row = fresh_db.fetchone(
|
|
114
|
-
"SELECT data_completeness FROM tushare_sync_state WHERE api_name='stock_basic'"
|
|
115
|
-
)
|
|
116
|
-
assert row is not None and row[0] == "final"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_tushare_calls_has_plugin_id_column(fresh_db: Database) -> None:
|
|
120
|
-
apply_core_migrations(fresh_db)
|
|
121
|
-
cols = {
|
|
122
|
-
r[0]
|
|
123
|
-
for r in fresh_db.fetchall(
|
|
124
|
-
"SELECT column_name FROM information_schema.columns WHERE table_name='tushare_calls'"
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
assert "plugin_id" in cols
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deeptrade_quant-0.4.0 → deeptrade_quant-0.4.2}/deeptrade/core/migrations/core/20260509_001_init.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|