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.
- finctx/__init__.py +24 -0
- finctx/_version.py +3 -0
- finctx/client.py +400 -0
- finctx/credentials.py +153 -0
- finctx/data/endpoints.json +1 -0
- finctx/data/marketdb-datasets.json +124 -0
- finctx/discovery.py +70 -0
- finctx/exceptions.py +110 -0
- finctx/generated/__init__.py +5 -0
- finctx/generated/api.py +28445 -0
- finctx/marketdb.py +544 -0
- finctx/py.typed +1 -0
- finctx/results.py +44 -0
- finctx/signing.py +71 -0
- finctx-0.3.0.dist-info/METADATA +197 -0
- finctx-0.3.0.dist-info/RECORD +18 -0
- finctx-0.3.0.dist-info/WHEEL +4 -0
- finctx-0.3.0.dist-info/licenses/LICENSE +186 -0
|
@@ -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
|
+
)
|