fxmacrodata-platform-core 0.1.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.
- fxmacrodata_platform_core/__init__.py +4 -0
- fxmacrodata_platform_core/core.py +268 -0
- fxmacrodata_platform_core/mcp-tools.json +4291 -0
- fxmacrodata_platform_core/operations.json +5770 -0
- fxmacrodata_platform_core/py.typed +0 -0
- fxmacrodata_platform_core-0.1.0.dist-info/METADATA +56 -0
- fxmacrodata_platform_core-0.1.0.dist-info/RECORD +10 -0
- fxmacrodata_platform_core-0.1.0.dist-info/WHEEL +5 -0
- fxmacrodata_platform_core-0.1.0.dist-info/licenses/LICENSE +21 -0
- fxmacrodata_platform_core-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"""Customer-side adapters for documented FXMacroData public contracts."""
|
|
2
|
+
from .core import Batch, COLUMNS, IntegrationError, operations, read, schemas, validate_request
|
|
3
|
+
|
|
4
|
+
__all__ = ["Batch", "COLUMNS", "IntegrationError", "operations", "read", "schemas", "validate_request"]
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Lossless result envelopes and bounded customer-side reads."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import hashlib
|
|
7
|
+
from importlib.resources import files
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import jsonschema
|
|
15
|
+
from fxmacrodata_public import FXMacroDataClient, Operation, Result
|
|
16
|
+
from fxmacrodata_public.client import SCHEMA_REGISTRY
|
|
17
|
+
from fxmacrodata_public.transport import redact_text
|
|
18
|
+
from fxmacrodata_public.pagination import iter_pages
|
|
19
|
+
|
|
20
|
+
COLUMNS = ("operation", "source_url", "record_id", "currency", "indicator", "date",
|
|
21
|
+
"announcement_datetime", "value", "value_text", "record_json", "response_json")
|
|
22
|
+
_FORBIDDEN = {"apikey", "accesstoken", "authorization", "password", "clientsecret",
|
|
23
|
+
"secret", "token", "url", "baseurl", "endpoint"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IntegrationError(RuntimeError):
|
|
27
|
+
"""A credential-free configuration or read failure."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def operations() -> tuple[Operation, ...]:
|
|
31
|
+
base = files(__package__)
|
|
32
|
+
rest = json.loads(base.joinpath("operations.json").read_text(encoding="utf-8"))
|
|
33
|
+
mcp = json.loads(base.joinpath("mcp-tools.json").read_text(encoding="utf-8"))
|
|
34
|
+
result = [Operation(**{**item, "parameters": tuple(item.get("parameters", []))}) for item in rest]
|
|
35
|
+
result.extend(Operation("mcp_" + t["name"], t.get("description", t["name"]),
|
|
36
|
+
t["inputSchema"], "/mcp", "MCP") for t in mcp)
|
|
37
|
+
return tuple(result)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def schemas() -> dict[str, dict]:
|
|
41
|
+
return {op.name: deepcopy(op.input_schema) for op in operations()}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _json(value: Any) -> str:
|
|
45
|
+
return json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _text(value: Any) -> str | None:
|
|
49
|
+
return None if value is None else str(value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Batch:
|
|
54
|
+
"""Original public response and additive tabular projections."""
|
|
55
|
+
operation: str
|
|
56
|
+
payload: Any
|
|
57
|
+
source_url: str = "https://fxmacrodata.com"
|
|
58
|
+
|
|
59
|
+
def records(self) -> list[dict]:
|
|
60
|
+
if isinstance(self.payload, dict) and isinstance(self.payload.get("versions"), list):
|
|
61
|
+
return deepcopy(self.payload["versions"])
|
|
62
|
+
if self.operation == "mcp_calendar_event_versions":
|
|
63
|
+
page = _cursor_page(self.payload)
|
|
64
|
+
if isinstance(page.get("versions"), list):
|
|
65
|
+
return deepcopy(page["versions"])
|
|
66
|
+
return Result(self.operation, self.payload, self.source_url).records()
|
|
67
|
+
|
|
68
|
+
def rows(self) -> list[dict]:
|
|
69
|
+
response = _json(self.payload)
|
|
70
|
+
output = []
|
|
71
|
+
for record in self.records() or [None]:
|
|
72
|
+
values = record if isinstance(record, dict) else {}
|
|
73
|
+
value = values.get("value", values.get("val"))
|
|
74
|
+
numeric = None
|
|
75
|
+
if value is not None and not isinstance(value, bool):
|
|
76
|
+
try:
|
|
77
|
+
parsed = float(value)
|
|
78
|
+
if math.isfinite(parsed):
|
|
79
|
+
numeric = parsed
|
|
80
|
+
except (TypeError, ValueError, OverflowError):
|
|
81
|
+
pass
|
|
82
|
+
encoded = _json(record)
|
|
83
|
+
identity = _json([self.operation, record if record is not None else self.payload])
|
|
84
|
+
output.append(dict(zip(COLUMNS, (
|
|
85
|
+
self.operation, self.source_url, hashlib.sha256(identity.encode()).hexdigest(),
|
|
86
|
+
_text(values.get("currency")), _text(values.get("indicator")),
|
|
87
|
+
_text(values.get("date")), _text(values.get("announcement_datetime")),
|
|
88
|
+
numeric, _text(value), encoded, response,
|
|
89
|
+
))))
|
|
90
|
+
return output
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _validate(value: Any) -> None:
|
|
94
|
+
if isinstance(value, dict):
|
|
95
|
+
for key, item in value.items():
|
|
96
|
+
if not isinstance(key, str) or re.sub(r"[_-]", "", key.lower()) in _FORBIDDEN:
|
|
97
|
+
raise IntegrationError("Use a runtime secret reference; credentials and endpoint overrides are not arguments.")
|
|
98
|
+
_validate(item)
|
|
99
|
+
elif isinstance(value, list):
|
|
100
|
+
for item in value:
|
|
101
|
+
_validate(item)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _redact(value: Any, secret: str) -> Any:
|
|
105
|
+
if isinstance(value, str):
|
|
106
|
+
return redact_text(value, secret)
|
|
107
|
+
if isinstance(value, list):
|
|
108
|
+
return [_redact(item, secret) for item in value]
|
|
109
|
+
if isinstance(value, dict):
|
|
110
|
+
return {_redact(str(k), secret): "[redacted]" if re.sub(r"[_-]", "", str(k).lower()) in _FORBIDDEN - {"url", "baseurl", "endpoint"}
|
|
111
|
+
else _redact(v, secret) for k, v in value.items()}
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def validate_request(operation: str, arguments: dict | None = None) -> dict:
|
|
116
|
+
"""Validate before a lazy source or pipeline can serialize arguments."""
|
|
117
|
+
inventory = {item.name: item for item in operations()}
|
|
118
|
+
if not isinstance(operation, str) or operation not in inventory:
|
|
119
|
+
raise IntegrationError("Unknown operation; inspect schemas() for supported operations.")
|
|
120
|
+
if arguments is not None and not isinstance(arguments, dict):
|
|
121
|
+
raise IntegrationError("Operation arguments must be an object.")
|
|
122
|
+
args = deepcopy(arguments or {})
|
|
123
|
+
_validate(args)
|
|
124
|
+
try:
|
|
125
|
+
jsonschema.Draft202012Validator(inventory[operation].input_schema, registry=SCHEMA_REGISTRY).validate(args)
|
|
126
|
+
except Exception:
|
|
127
|
+
raise IntegrationError("Invalid arguments; inspect the operation's input schema.") from None
|
|
128
|
+
return args
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _HeaderClient(FXMacroDataClient):
|
|
132
|
+
"""Use public API-key headers rather than credentials in request URLs."""
|
|
133
|
+
def _auth_params(self):
|
|
134
|
+
return {}
|
|
135
|
+
|
|
136
|
+
def _request(self, method, url, *, headers=None, **kwargs):
|
|
137
|
+
headers = dict(headers or {})
|
|
138
|
+
if self._api_key:
|
|
139
|
+
headers["X-API-Key"] = self._api_key
|
|
140
|
+
return super()._request(method, url, headers=headers, **kwargs)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _cursor_page(payload):
|
|
144
|
+
if isinstance(payload, dict) and "checkpoint" in payload:
|
|
145
|
+
return payload
|
|
146
|
+
if isinstance(payload, dict):
|
|
147
|
+
for name in ("structuredContent", "result"):
|
|
148
|
+
if isinstance(payload.get(name), dict):
|
|
149
|
+
return _cursor_page(payload[name])
|
|
150
|
+
for block in payload.get("content", []):
|
|
151
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
152
|
+
try:
|
|
153
|
+
candidate = json.loads(block["text"])
|
|
154
|
+
except (KeyError, TypeError, ValueError):
|
|
155
|
+
continue
|
|
156
|
+
if isinstance(candidate, dict) and "checkpoint" in candidate:
|
|
157
|
+
return candidate
|
|
158
|
+
raise IntegrationError("Calendar response omitted its archive checkpoint.")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _cursor_history(client, operation, args, max_pages):
|
|
162
|
+
if args.get("cursor"):
|
|
163
|
+
raise IntegrationError("Complete calendar history starts without a cursor.")
|
|
164
|
+
field = "versions" if "event_versions" in operation else "events"
|
|
165
|
+
query, pages, rows, seen = dict(args), [], [], set()
|
|
166
|
+
checkpoint = args.get("checkpoint")
|
|
167
|
+
source = "https://fxmacrodata.com"
|
|
168
|
+
for _ in range(max_pages):
|
|
169
|
+
result = client.execute(operation, query)
|
|
170
|
+
page = _cursor_page(result.payload)
|
|
171
|
+
current = page.get("checkpoint")
|
|
172
|
+
if type(current) is not int or (checkpoint is not None and current != checkpoint):
|
|
173
|
+
raise IntegrationError("Calendar archive checkpoint changed; restart the read.")
|
|
174
|
+
checkpoint = current
|
|
175
|
+
data = page.get(field)
|
|
176
|
+
if not isinstance(data, list):
|
|
177
|
+
raise IntegrationError("Calendar response omitted its record array.")
|
|
178
|
+
pages.append(deepcopy(result.payload))
|
|
179
|
+
rows.extend(deepcopy(data))
|
|
180
|
+
source = result.source_url
|
|
181
|
+
cursor = page.get("next_cursor")
|
|
182
|
+
if cursor is None:
|
|
183
|
+
return Result(operation, {field: rows, "complete": True, "checkpoint": checkpoint,
|
|
184
|
+
"next_cursor": None, "pages": pages}, source)
|
|
185
|
+
if not isinstance(cursor, str) or not cursor or cursor in seen or not data:
|
|
186
|
+
raise IntegrationError("Calendar cursor did not advance; history is incomplete.")
|
|
187
|
+
seen.add(cursor)
|
|
188
|
+
query.update(cursor=cursor, checkpoint=checkpoint)
|
|
189
|
+
raise IntegrationError("Calendar history exceeded the explicit page budget.")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _offset_history(client, operation, args, max_pages):
|
|
193
|
+
pages, rows, source = [], [], "https://fxmacrodata.com"
|
|
194
|
+
query = dict(args)
|
|
195
|
+
query.pop("page", None)
|
|
196
|
+
def fetch(arguments):
|
|
197
|
+
nonlocal source
|
|
198
|
+
result = client.execute(operation, arguments)
|
|
199
|
+
source = result.source_url
|
|
200
|
+
pages.append(deepcopy(result.payload))
|
|
201
|
+
return result.payload
|
|
202
|
+
checkpoint = None
|
|
203
|
+
for page in iter_pages(fetch, query, max_pages=max_pages, request_id=operation, require_pagination=True):
|
|
204
|
+
rows.extend(deepcopy(page["data"]))
|
|
205
|
+
checkpoint = page["client_resume"]
|
|
206
|
+
return Result(operation, {"data":rows, "complete":True, "pages":pages,
|
|
207
|
+
"client_resume":checkpoint}, source)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def read(operation: str, arguments: dict | None = None, *, api_key_env: str | None = None,
|
|
211
|
+
complete_history: bool = True, max_pages: int = 10000, client_factory=None) -> Batch:
|
|
212
|
+
"""Read a documented operation without serializing or logging a credential.
|
|
213
|
+
|
|
214
|
+
Omitting api_key_env explicitly selects anonymous access, even when an
|
|
215
|
+
unrelated API key exists in the environment. Every result retains its
|
|
216
|
+
original envelope; empty results still have one metadata envelope row.
|
|
217
|
+
"""
|
|
218
|
+
inventory = {item.name: item for item in operations()}
|
|
219
|
+
if not isinstance(operation, str) or operation not in inventory:
|
|
220
|
+
raise IntegrationError("Unknown operation; inspect schemas() for supported operations.")
|
|
221
|
+
if arguments is not None and not isinstance(arguments, dict):
|
|
222
|
+
raise IntegrationError("Operation arguments must be an object.")
|
|
223
|
+
args = validate_request(operation, arguments)
|
|
224
|
+
if type(max_pages) is not int or not 1 <= max_pages <= 10000 or type(complete_history) is not bool:
|
|
225
|
+
raise IntegrationError("Use a boolean history option and page budget between 1 and 10000.")
|
|
226
|
+
secret = ""
|
|
227
|
+
if api_key_env is not None:
|
|
228
|
+
if not isinstance(api_key_env, str) or not re.fullmatch(r"[A-Z][A-Z0-9_]{0,127}", api_key_env):
|
|
229
|
+
raise IntegrationError("Use an environment-variable name for the API key.")
|
|
230
|
+
secret = os.environ.get(api_key_env, "")
|
|
231
|
+
if not secret:
|
|
232
|
+
raise IntegrationError("The named API-key environment variable is unavailable.")
|
|
233
|
+
selected = inventory[operation]
|
|
234
|
+
try:
|
|
235
|
+
jsonschema.Draft202012Validator(selected.input_schema, registry=SCHEMA_REGISTRY).validate(args)
|
|
236
|
+
except Exception:
|
|
237
|
+
raise IntegrationError("Invalid arguments; inspect the operation's input schema.") from None
|
|
238
|
+
paginated = selected.method == "GET" and any(p["name"] == "offset" for p in selected.parameters)
|
|
239
|
+
if complete_history and paginated and (args.get("offset", 0) != 0 or args.get("page", 1) != 1):
|
|
240
|
+
raise IntegrationError("Complete history starts at offset zero; use complete_history=False for an individual page.")
|
|
241
|
+
factory = client_factory or _HeaderClient
|
|
242
|
+
client = None
|
|
243
|
+
try:
|
|
244
|
+
client = factory(api_key=secret)
|
|
245
|
+
# The dependency is a public client with a smaller packaged catalogue.
|
|
246
|
+
# Supplement its public descriptors without importing service code.
|
|
247
|
+
if isinstance(client, FXMacroDataClient):
|
|
248
|
+
client._operations = inventory
|
|
249
|
+
if complete_history and operation in {"calendar_snapshot", "calendar_event_versions", "mcp_calendar_snapshot", "mcp_calendar_event_versions"}:
|
|
250
|
+
result = _cursor_history(client, operation, args, max_pages)
|
|
251
|
+
elif complete_history and paginated:
|
|
252
|
+
result = _offset_history(client, operation, args, max_pages)
|
|
253
|
+
else:
|
|
254
|
+
result = client.execute(operation, args)
|
|
255
|
+
payload = _redact(deepcopy(result.payload), secret)
|
|
256
|
+
# Fail closed on invalid JSON rather than storing platform-specific guesses.
|
|
257
|
+
_json(payload)
|
|
258
|
+
return Batch(operation, payload, _redact(result.source_url, secret))
|
|
259
|
+
except IntegrationError:
|
|
260
|
+
raise
|
|
261
|
+
except Exception:
|
|
262
|
+
raise IntegrationError("FXMacroData read failed; check credentials, operation arguments and service availability.") from None
|
|
263
|
+
finally:
|
|
264
|
+
if client is not None:
|
|
265
|
+
try:
|
|
266
|
+
client.close()
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|