finctx 0.3.0__py3-none-any.whl

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,124 @@
1
+ {
2
+ "datasets": {
3
+ "a_share_daily": {
4
+ "endpoint": "daily",
5
+ "keys": [
6
+ "code",
7
+ "trade_date"
8
+ ],
9
+ "kwargs": {}
10
+ },
11
+ "adj_factor": {
12
+ "endpoint": "adj_factor",
13
+ "keys": [
14
+ "code",
15
+ "trade_date"
16
+ ],
17
+ "kwargs": {}
18
+ },
19
+ "bond_daily": {
20
+ "endpoint": "bond_daily",
21
+ "keys": [
22
+ "code",
23
+ "trade_date"
24
+ ],
25
+ "kwargs": {}
26
+ },
27
+ "bond_dailyprice": {
28
+ "endpoint": "get_bond_dailyprice",
29
+ "keys": [
30
+ "code",
31
+ "trade_date"
32
+ ],
33
+ "kwargs": {
34
+ "count_type": "1",
35
+ "fields": []
36
+ }
37
+ },
38
+ "cb_daily": {
39
+ "endpoint": "cb_daily",
40
+ "keys": [
41
+ "code",
42
+ "trade_date"
43
+ ],
44
+ "kwargs": {}
45
+ },
46
+ "concept_index_daily": {
47
+ "endpoint": "concept_index_daily",
48
+ "keys": [
49
+ "concept_code",
50
+ "trade_date"
51
+ ],
52
+ "kwargs": {}
53
+ },
54
+ "fund_daily": {
55
+ "endpoint": "fund_daily",
56
+ "keys": [
57
+ "code",
58
+ "trade_date"
59
+ ],
60
+ "kwargs": {}
61
+ },
62
+ "fund_dailyprice": {
63
+ "endpoint": "get_fund_dailyprice",
64
+ "keys": [
65
+ "code",
66
+ "trade_date"
67
+ ],
68
+ "kwargs": {
69
+ "adjust_type": "N",
70
+ "count_type": "1",
71
+ "fields": []
72
+ }
73
+ },
74
+ "fut_daily": {
75
+ "endpoint": "fut_daily",
76
+ "keys": [
77
+ "code",
78
+ "trade_date"
79
+ ],
80
+ "kwargs": {}
81
+ },
82
+ "hk_daily": {
83
+ "endpoint": "hk_daily",
84
+ "keys": [
85
+ "code",
86
+ "trade_date"
87
+ ],
88
+ "kwargs": {}
89
+ },
90
+ "index_daily": {
91
+ "endpoint": "index_daily",
92
+ "keys": [
93
+ "code",
94
+ "trade_date"
95
+ ],
96
+ "kwargs": {}
97
+ },
98
+ "opt_daily": {
99
+ "endpoint": "opt_daily",
100
+ "keys": [
101
+ "code",
102
+ "trade_date"
103
+ ],
104
+ "kwargs": {}
105
+ },
106
+ "sge_daily": {
107
+ "endpoint": "sge_daily",
108
+ "keys": [
109
+ "code",
110
+ "trade_date"
111
+ ],
112
+ "kwargs": {}
113
+ },
114
+ "trade_cal": {
115
+ "endpoint": "trade_cal",
116
+ "keys": [
117
+ "exchange",
118
+ "calendar_date"
119
+ ],
120
+ "kwargs": {}
121
+ }
122
+ },
123
+ "schema_version": 1
124
+ }
finctx/discovery.py ADDED
@@ -0,0 +1,70 @@
1
+ """Offline endpoint and response-field discovery from the packaged registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ import difflib
7
+ from functools import lru_cache
8
+ from importlib import resources
9
+ import json
10
+ from typing import Any, Optional
11
+
12
+
13
+ @lru_cache(maxsize=1)
14
+ def _registry() -> dict[str, Any]:
15
+ # ``files`` is available on Python 3.9 and works for wheel/zip resources.
16
+ resource = resources.files("finctx").joinpath("data").joinpath("endpoints.json")
17
+ with resource.open("r", encoding="utf-8") as handle:
18
+ document = json.load(handle)
19
+ if not isinstance(document, dict) or not isinstance(document.get("endpoints"), list):
20
+ raise ValueError("packaged Finctx endpoint registry has an invalid structure")
21
+ return document
22
+
23
+
24
+ @lru_cache(maxsize=1)
25
+ def _by_name() -> dict[str, dict[str, Any]]:
26
+ entries = _registry()["endpoints"]
27
+ index = {entry["name"]: entry for entry in entries}
28
+ if len(index) != len(entries):
29
+ raise ValueError("packaged Finctx endpoint registry contains duplicate names")
30
+ return index
31
+
32
+
33
+ def catalog_document() -> dict[str, Any]:
34
+ """Return the full registry document, including catalog-level provenance."""
35
+
36
+ return deepcopy(_registry())
37
+
38
+
39
+ def endpoints() -> dict[str, dict[str, Any]]:
40
+ """Return every endpoint keyed by its exact generated method name."""
41
+
42
+ return deepcopy(_by_name())
43
+
44
+
45
+ def fields(endpoint: str, search: Optional[str] = None) -> list[dict[str, Any]]:
46
+ """Return documented response fields for one exact generated method name."""
47
+
48
+ if not isinstance(endpoint, str) or not endpoint:
49
+ raise ValueError("endpoint must be a non-empty method name")
50
+ index = _by_name()
51
+ entry = index.get(endpoint)
52
+ if entry is None:
53
+ guesses = difflib.get_close_matches(endpoint, index.keys(), n=3, cutoff=0.5)
54
+ hint = f"; did you mean {', '.join(guesses)}?" if guesses else ""
55
+ raise ValueError(f"unknown Finctx endpoint {endpoint!r}{hint}")
56
+ if search is not None and not isinstance(search, str):
57
+ raise TypeError("search must be a string or None")
58
+ documented = entry["response"]["fields"]
59
+ if not search or not search.strip():
60
+ return deepcopy(documented)
61
+ term = search.strip().casefold()
62
+ return deepcopy(
63
+ [
64
+ field
65
+ for field in documented
66
+ if term in field["name"].casefold()
67
+ or term in field.get("description", "").casefold()
68
+ or term in field.get("type", "").casefold()
69
+ ]
70
+ )
finctx/exceptions.py ADDED
@@ -0,0 +1,110 @@
1
+ """Exceptions raised by the Finctx Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ import requests
8
+
9
+
10
+ class FinctxError(Exception):
11
+ """Base class for all SDK-specific exceptions."""
12
+
13
+
14
+ class FinctxTransportError(FinctxError):
15
+ """An HTTP or WebSocket transport operation could not be completed."""
16
+
17
+ def __init__(self, message: str, *, cause: BaseException) -> None:
18
+ super().__init__(message)
19
+ self.cause = cause
20
+
21
+
22
+ class FinctxWebSocketMessageError(FinctxError):
23
+ """A WebSocket message did not match the documented JSON text protocol."""
24
+
25
+ def __init__(self, message: str, *, raw_message: Any) -> None:
26
+ super().__init__(message)
27
+ self.raw_message = raw_message
28
+
29
+
30
+ class FinctxApiError(FinctxError):
31
+ """The Finctx endpoint returned a non-success HTTP status."""
32
+
33
+ def __init__(
34
+ self,
35
+ *,
36
+ status_code: int,
37
+ error_code: Optional[str],
38
+ message: str,
39
+ request_id: Optional[str],
40
+ response: requests.Response,
41
+ ) -> None:
42
+ details = f"{status_code} {message}"
43
+ if error_code:
44
+ details += f" (error={error_code})"
45
+ if request_id:
46
+ details += f" (request_id={request_id})"
47
+ super().__init__(details)
48
+ self.status_code = status_code
49
+ self.error_code = error_code
50
+ self.message = message
51
+ self.request_id = request_id
52
+ self.response = response
53
+
54
+
55
+ class FinctxResponseError(FinctxError):
56
+ """A successful API response did not contain the JSON expected by an API function."""
57
+
58
+ def __init__(self, message: str, *, response: requests.Response) -> None:
59
+ super().__init__(message)
60
+ self.response = response
61
+ self.request_id = response.headers.get("X-Meridian-Request-ID")
62
+
63
+
64
+ class FinctxResultError(FinctxError):
65
+ """A JSON result cannot be interpreted as a table of rows."""
66
+
67
+ def __init__(self, message: str, *, result: Any) -> None:
68
+ super().__init__(message)
69
+ self.result = result
70
+ raw_error = result.get("error") if isinstance(result, dict) else None
71
+ self.error_code = (
72
+ raw_error
73
+ if isinstance(raw_error, str)
74
+ else raw_error.get("code")
75
+ if isinstance(raw_error, dict)
76
+ else None
77
+ )
78
+
79
+
80
+ def api_error_from_response(response: requests.Response) -> FinctxApiError:
81
+ """Create a stable, structured error without exposing an arbitrary response body."""
82
+
83
+ payload: Any = None
84
+ try:
85
+ payload = response.json()
86
+ except ValueError:
87
+ pass
88
+
89
+ error_code: Optional[str] = None
90
+ message: Optional[str] = None
91
+ if isinstance(payload, dict):
92
+ raw_error = payload.get("error")
93
+ if isinstance(raw_error, str):
94
+ error_code = raw_error
95
+ elif isinstance(raw_error, dict):
96
+ raw_code = raw_error.get("code")
97
+ if isinstance(raw_code, str):
98
+ error_code = raw_code
99
+
100
+ raw_message = payload.get("message")
101
+ if isinstance(raw_message, str) and raw_message:
102
+ message = raw_message
103
+
104
+ return FinctxApiError(
105
+ status_code=response.status_code,
106
+ error_code=error_code,
107
+ message=message or response.reason or "Finctx API request failed",
108
+ request_id=response.headers.get("X-Meridian-Request-ID"),
109
+ response=response,
110
+ )
@@ -0,0 +1,5 @@
1
+ """自动生成的 legacy 业务函数。"""
2
+
3
+ from .api import GeneratedApiMixin
4
+
5
+ __all__ = ["GeneratedApiMixin"]