fxmacrodata-platform-core 0.1.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.
- fxmacrodata_platform_core-0.1.0/LICENSE +21 -0
- fxmacrodata_platform_core-0.1.0/MANIFEST.in +11 -0
- fxmacrodata_platform_core-0.1.0/PKG-INFO +56 -0
- fxmacrodata_platform_core-0.1.0/PUBLIC_FILES.json +13 -0
- fxmacrodata_platform_core-0.1.0/README.md +45 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core/__init__.py +4 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core/core.py +268 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core/mcp-tools.json +4291 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core/operations.json +5770 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core/py.typed +0 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core.egg-info/PKG-INFO +56 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core.egg-info/SOURCES.txt +16 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core.egg-info/dependency_links.txt +1 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core.egg-info/requires.txt +1 -0
- fxmacrodata_platform_core-0.1.0/fxmacrodata_platform_core.egg-info/top_level.txt +1 -0
- fxmacrodata_platform_core-0.1.0/pyproject.toml +20 -0
- fxmacrodata_platform_core-0.1.0/setup.cfg +4 -0
- fxmacrodata_platform_core-0.1.0/tests/test_core.py +154 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Tidball
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
include PUBLIC_FILES.json
|
|
5
|
+
include MANIFEST.in
|
|
6
|
+
include fxmacrodata_platform_core/__init__.py
|
|
7
|
+
include fxmacrodata_platform_core/core.py
|
|
8
|
+
include fxmacrodata_platform_core/operations.json
|
|
9
|
+
include fxmacrodata_platform_core/mcp-tools.json
|
|
10
|
+
include fxmacrodata_platform_core/py.typed
|
|
11
|
+
include tests/test_core.py
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fxmacrodata-platform-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Public-contract data adapters for FXMacroData platform integrations
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: fxmacrodata-public-client==0.1.1
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# FXMacroData platform core
|
|
13
|
+
|
|
14
|
+
Read documented FXMacroData operations into lossless records for data platforms.
|
|
15
|
+
The package adds a stable tabular projection while preserving complete response
|
|
16
|
+
envelopes, source links, original values, release timestamps and revisions.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from fxmacrodata_platform_core import read, schemas
|
|
20
|
+
|
|
21
|
+
batch = read("release_calendar", {"currency": "usd"})
|
|
22
|
+
records = batch.records()
|
|
23
|
+
table_rows = batch.rows()
|
|
24
|
+
original_response = batch.payload
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
USD catalogue, recent macro history and calendar provide a no-key evaluation
|
|
28
|
+
workflow subject to the public service's limits and delay. For subscribed access,
|
|
29
|
+
provision a secret environment variable and pass only its name:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
batch = read("indicator_history", {"currency": "eur", "indicator": "policy_rate"},
|
|
33
|
+
api_key_env="FXMACRODATA_API_KEY")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Omitting `api_key_env` explicitly uses anonymous access. Credentials are sent in
|
|
37
|
+
headers, never operation arguments. Errors omit provider response text and URLs.
|
|
38
|
+
The package sends requests only through the public client to documented service
|
|
39
|
+
origins.
|
|
40
|
+
|
|
41
|
+
`schemas()` returns every packaged operation's input schema. Offset-paginated
|
|
42
|
+
REST operations download complete available histories within `max_pages` and
|
|
43
|
+
reject partial initial offsets. Calendar snapshot/version operations follow cursors
|
|
44
|
+
within the same archive checkpoint, retaining every original page. Other MCP tools
|
|
45
|
+
and finite SSE captures preserve their bounded public response. Use
|
|
46
|
+
`complete_history=False` for an individual history page or explicit continuation.
|
|
47
|
+
|
|
48
|
+
`record_json` and `response_json` preserve parsed public JSON except credential
|
|
49
|
+
redaction. JSON number tokens use the public Python client's numeric parser;
|
|
50
|
+
their original lexical precision is not guaranteed. Source numbers supplied as
|
|
51
|
+
strings retain their precision. `value` is an approximate floating-point
|
|
52
|
+
convenience; `value_text` represents the parsed source value. Empty responses produce one envelope
|
|
53
|
+
row with `record_json` set to JSON `null`. No publication time or future date is
|
|
54
|
+
inferred. A stable content hash identifies repeated records, without a fetch clock.
|
|
55
|
+
|
|
56
|
+
[FXMacroData documentation](https://fxmacrodata.com/documentation/reference?utm_source=github&utm_medium=referral&utm_campaign=platform-core&utm_content=readme)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[
|
|
2
|
+
"LICENSE",
|
|
3
|
+
"README.md",
|
|
4
|
+
"pyproject.toml",
|
|
5
|
+
"PUBLIC_FILES.json",
|
|
6
|
+
"MANIFEST.in",
|
|
7
|
+
"fxmacrodata_platform_core/__init__.py",
|
|
8
|
+
"fxmacrodata_platform_core/core.py",
|
|
9
|
+
"fxmacrodata_platform_core/operations.json",
|
|
10
|
+
"fxmacrodata_platform_core/mcp-tools.json",
|
|
11
|
+
"fxmacrodata_platform_core/py.typed",
|
|
12
|
+
"tests/test_core.py"
|
|
13
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# FXMacroData platform core
|
|
2
|
+
|
|
3
|
+
Read documented FXMacroData operations into lossless records for data platforms.
|
|
4
|
+
The package adds a stable tabular projection while preserving complete response
|
|
5
|
+
envelopes, source links, original values, release timestamps and revisions.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from fxmacrodata_platform_core import read, schemas
|
|
9
|
+
|
|
10
|
+
batch = read("release_calendar", {"currency": "usd"})
|
|
11
|
+
records = batch.records()
|
|
12
|
+
table_rows = batch.rows()
|
|
13
|
+
original_response = batch.payload
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
USD catalogue, recent macro history and calendar provide a no-key evaluation
|
|
17
|
+
workflow subject to the public service's limits and delay. For subscribed access,
|
|
18
|
+
provision a secret environment variable and pass only its name:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
batch = read("indicator_history", {"currency": "eur", "indicator": "policy_rate"},
|
|
22
|
+
api_key_env="FXMACRODATA_API_KEY")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Omitting `api_key_env` explicitly uses anonymous access. Credentials are sent in
|
|
26
|
+
headers, never operation arguments. Errors omit provider response text and URLs.
|
|
27
|
+
The package sends requests only through the public client to documented service
|
|
28
|
+
origins.
|
|
29
|
+
|
|
30
|
+
`schemas()` returns every packaged operation's input schema. Offset-paginated
|
|
31
|
+
REST operations download complete available histories within `max_pages` and
|
|
32
|
+
reject partial initial offsets. Calendar snapshot/version operations follow cursors
|
|
33
|
+
within the same archive checkpoint, retaining every original page. Other MCP tools
|
|
34
|
+
and finite SSE captures preserve their bounded public response. Use
|
|
35
|
+
`complete_history=False` for an individual history page or explicit continuation.
|
|
36
|
+
|
|
37
|
+
`record_json` and `response_json` preserve parsed public JSON except credential
|
|
38
|
+
redaction. JSON number tokens use the public Python client's numeric parser;
|
|
39
|
+
their original lexical precision is not guaranteed. Source numbers supplied as
|
|
40
|
+
strings retain their precision. `value` is an approximate floating-point
|
|
41
|
+
convenience; `value_text` represents the parsed source value. Empty responses produce one envelope
|
|
42
|
+
row with `record_json` set to JSON `null`. No publication time or future date is
|
|
43
|
+
inferred. A stable content hash identifies repeated records, without a fetch clock.
|
|
44
|
+
|
|
45
|
+
[FXMacroData documentation](https://fxmacrodata.com/documentation/reference?utm_source=github&utm_medium=referral&utm_campaign=platform-core&utm_content=readme)
|
|
@@ -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
|