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.
@@ -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