dataquery-sdk 1.2.2__tar.gz → 1.2.4__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.
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/CHANGELOG.md +9 -2
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/PKG-INFO +4 -4
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/__init__.py +10 -16
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/cli.py +96 -19
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/config/env.py +114 -35
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/config/logging.py +3 -30
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/constants/__init__.py +1 -13
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/constants/api.py +1 -5
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/constants/download.py +0 -9
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/core/_mixins.py +92 -113
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/core/_sync.py +2 -21
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/core/client.py +92 -290
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/dataquery.py +116 -529
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/download/__init__.py +1 -2
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/download/parallel.py +7 -61
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/download/utils.py +3 -19
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/export.py +2 -10
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/function_registry.py +1 -9
- dataquery_sdk-1.2.4/dataquery/mcp_proxy.py +265 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/sse/__init__.py +1 -2
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/sse/client.py +15 -146
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/sse/event_store.py +8 -56
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/sse/subscriber.py +12 -185
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/transport/__init__.py +1 -2
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/transport/auth.py +9 -39
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/transport/connection_pool.py +3 -23
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/transport/rate_limiter.py +4 -41
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/transport/retry.py +5 -48
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/types/exceptions.py +14 -3
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/types/models.py +54 -56
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/utils.py +24 -232
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/PKG-INFO +4 -4
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/SOURCES.txt +1 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/requires.txt +3 -3
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/pyproject.toml +4 -4
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/CONTRIBUTING.md +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/LICENSE +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/MANIFEST.in +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/README.md +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/config/__init__.py +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/constants/rate_limit.py +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/constants/sse.py +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/core/__init__.py +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/data/function.json +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/py.typed +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery/types/__init__.py +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/dependency_links.txt +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/entry_points.txt +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/top_level.txt +0 -0
- {dataquery_sdk-1.2.2 → dataquery_sdk-1.2.4}/setup.cfg +0 -0
|
@@ -46,5 +46,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
46
46
|
- Extraction is Zip Slip-guarded, skips current-day archives, and surfaces failures via `extraction_errors` (downgrading overall status to `partial`); date windows with no available files no longer mark a multi-window run as `partial`
|
|
47
47
|
## [1.2.2] - 2026-07-18
|
|
48
48
|
- MCP: new `mcp-connect` CLI command
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
## [1.2.3] - 2026-07-23
|
|
50
|
+
- Pagination: new client-driven `get_next_page_async(page)` helper — read `next_link` off any paged response and hand the page back to fetch the next one (manual counterpart to the SDK-driven `iter_pages`); next-page links are resolved against the surface the page came from and never sent off-host
|
|
51
|
+
- Pagination models: `items`, `page-size`, and `info` fields, plus the `next_link` property and `get_self_link()` accessor, moved onto the shared `Paginated` mixin; `FileList` is now paginated; list fields default to empty instead of being required so partial/empty envelopes parse cleanly
|
|
52
|
+
- New `APIResponseError` raised when a 2xx response carries an `errors`/`error` envelope (e.g. `498 Unrecognized Page Token`); an `info` `204` "no content" envelope now yields an empty page so pagination stops cleanly, while any other unrecognized body fails loudly
|
|
53
|
+
- Exports: `Paginated`, `Link`, `APIResponseError` are now part of the public API
|
|
54
|
+
## [1.2.4] - 2026-08-12
|
|
55
|
+
- MCP: `mcp-connect` accepts `--client-id`, `--client-secret` and `--bearer-token` and exports them into the `DATAQUERY_*` environment of the process, so the MCP bridge and the SDK share one credential set instead of each needing its own setup
|
|
56
|
+
|
|
57
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataquery-sdk
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Python SDK for DATAQUERY Data API - Query, download, and check availability of economic data files
|
|
5
5
|
Author-email: DATAQUERY SDK Team <dataquery_support@jpmorgan.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/jpmorganchase/dataquery-sdk
|
|
@@ -25,7 +25,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
|
25
25
|
Requires-Python: >=3.12
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
|
-
Requires-Dist: aiohttp<4.0.0,>=3.
|
|
28
|
+
Requires-Dist: aiohttp<4.0.0,>=3.14.3
|
|
29
29
|
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
30
30
|
Requires-Dist: structlog>=23.0.0
|
|
31
31
|
Requires-Dist: python-dotenv>=1.0.0
|
|
@@ -46,7 +46,7 @@ Requires-Dist: mkdocs>=1.5.0; extra == "docs"
|
|
|
46
46
|
Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
|
|
47
47
|
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "docs"
|
|
48
48
|
Requires-Dist: mike>=1.1.0; extra == "docs"
|
|
49
|
-
Requires-Dist: pymdown-extensions>=
|
|
49
|
+
Requires-Dist: pymdown-extensions>=11.0.0; extra == "docs"
|
|
50
50
|
Provides-Extra: pandas
|
|
51
51
|
Requires-Dist: pandas>=2.0.0; extra == "pandas"
|
|
52
52
|
Provides-Extra: mcp
|
|
@@ -67,7 +67,7 @@ Requires-Dist: fastmcp>=2.14; extra == "all"
|
|
|
67
67
|
Requires-Dist: pip-audit>=2.7.0; extra == "all"
|
|
68
68
|
Requires-Dist: build>=1.3.0; extra == "all"
|
|
69
69
|
Requires-Dist: urllib3>=2.7.0; extra == "all"
|
|
70
|
-
Requires-Dist: pymdown-extensions>=
|
|
70
|
+
Requires-Dist: pymdown-extensions>=11.0.0; extra == "all"
|
|
71
71
|
Dynamic: license-file
|
|
72
72
|
|
|
73
73
|
# DataQuery SDK
|
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
"""
|
|
2
|
-
DATAQUERY SDK - Python SDK for DATAQUERY Data API
|
|
1
|
+
"""DATAQUERY SDK - Python SDK for DATAQUERY Data API."""
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
to economic data files with advanced features like querying, downloading, availability
|
|
6
|
-
checking, rate limiting, retry logic, connection pool monitoring, and comprehensive logging.
|
|
7
|
-
|
|
8
|
-
Quick Start:
|
|
9
|
-
>>> from dataquery import DataQuery
|
|
10
|
-
>>> async with DataQuery() as dq:
|
|
11
|
-
... groups = await dq.list_groups_async()
|
|
12
|
-
... print(f"Found {len(groups)} groups")
|
|
13
|
-
|
|
14
|
-
For more information, visit: https://github.com/jpmorganchase/dataquery-sdk
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
__version__ = "1.2.2"
|
|
3
|
+
__version__ = "1.2.4"
|
|
18
4
|
__author__ = "DATAQUERY SDK Team"
|
|
19
5
|
__email__ = "dataquery_support@jpmorgan.com"
|
|
20
6
|
__license__ = "MIT"
|
|
@@ -34,6 +20,7 @@ from .function_registry import (
|
|
|
34
20
|
from .sse.client import SSEClient, SSEEvent
|
|
35
21
|
from .sse.subscriber import NotificationDownloadManager
|
|
36
22
|
from .types.exceptions import (
|
|
23
|
+
APIResponseError,
|
|
37
24
|
AuthenticationError,
|
|
38
25
|
AvailabilityError,
|
|
39
26
|
ConfigurationError,
|
|
@@ -45,6 +32,7 @@ from .types.exceptions import (
|
|
|
45
32
|
GroupNotFoundError,
|
|
46
33
|
NetworkError,
|
|
47
34
|
NotFoundError,
|
|
35
|
+
PaginationError,
|
|
48
36
|
RateLimitError,
|
|
49
37
|
ValidationError,
|
|
50
38
|
WorkflowError,
|
|
@@ -62,6 +50,8 @@ from .types.models import (
|
|
|
62
50
|
FileList,
|
|
63
51
|
Group,
|
|
64
52
|
GroupList,
|
|
53
|
+
Link,
|
|
54
|
+
Paginated,
|
|
65
55
|
)
|
|
66
56
|
from .utils import download_zip_async
|
|
67
57
|
|
|
@@ -71,6 +61,8 @@ __all__ = [
|
|
|
71
61
|
"ClientConfig",
|
|
72
62
|
"Group",
|
|
73
63
|
"GroupList",
|
|
64
|
+
"Paginated",
|
|
65
|
+
"Link",
|
|
74
66
|
"FileInfo",
|
|
75
67
|
"FileList",
|
|
76
68
|
"AvailabilityInfo",
|
|
@@ -81,9 +73,11 @@ __all__ = [
|
|
|
81
73
|
"DownloadProgress",
|
|
82
74
|
"DateRange",
|
|
83
75
|
"DataQueryError",
|
|
76
|
+
"APIResponseError",
|
|
84
77
|
"AuthenticationError",
|
|
85
78
|
"ValidationError",
|
|
86
79
|
"NotFoundError",
|
|
80
|
+
"PaginationError",
|
|
87
81
|
"RateLimitError",
|
|
88
82
|
"NetworkError",
|
|
89
83
|
"ConfigurationError",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import argparse
|
|
4
4
|
import asyncio
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
from collections.abc import AsyncGenerator
|
|
8
9
|
from pathlib import Path
|
|
@@ -11,8 +12,6 @@ from typing import Any, Dict, List, Optional
|
|
|
11
12
|
from dataquery import DataQuery
|
|
12
13
|
from dataquery.types.exceptions import DataQueryError
|
|
13
14
|
|
|
14
|
-
# ── Output helpers (legacy-CLI "summary + --- JSON ---" format) ────────────
|
|
15
|
-
|
|
16
15
|
|
|
17
16
|
def _to_dict(payload: Any) -> Dict[str, Any]:
|
|
18
17
|
"""Normalize a Pydantic model or dict into a plain dict for JSON dump."""
|
|
@@ -82,6 +81,64 @@ def _print_error(message: str, *, suggestion: Optional[str] = None) -> None:
|
|
|
82
81
|
print(json.dumps(envelope, indent=2))
|
|
83
82
|
|
|
84
83
|
|
|
84
|
+
_MCP_CREDENTIAL_FLAGS = (
|
|
85
|
+
("client_id", "CLIENT_ID"),
|
|
86
|
+
("client_secret", "CLIENT_SECRET"),
|
|
87
|
+
("bearer_token", "BEARER_TOKEN"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Endpoint settings saved alongside the credentials, so a later DataQuery()
|
|
91
|
+
# mints tokens against the same environment instead of the PROD defaults.
|
|
92
|
+
_MCP_SAVED_SETTINGS = ("BASE_URL", "OAUTH_TOKEN_URL", "OAUTH_AUD")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _export_mcp_credentials(args: argparse.Namespace) -> List[str]:
|
|
96
|
+
"""Export ``mcp-connect`` credential flags into the ``DATAQUERY_*`` process env.
|
|
97
|
+
|
|
98
|
+
Flags win over anything already exported, so the rest of this process —
|
|
99
|
+
the token manager here, and any SDK use later on — resolves them the usual
|
|
100
|
+
way with no separate environment setup. Returns the unprefixed keys that
|
|
101
|
+
are actually set afterwards, in save order.
|
|
102
|
+
"""
|
|
103
|
+
from dataquery.config import EnvConfig
|
|
104
|
+
|
|
105
|
+
for attr, env_key in _MCP_CREDENTIAL_FLAGS:
|
|
106
|
+
value = getattr(args, attr, None)
|
|
107
|
+
if value:
|
|
108
|
+
os.environ[f"{EnvConfig.PREFIX}{env_key}"] = value
|
|
109
|
+
|
|
110
|
+
keys = [env_key for _, env_key in _MCP_CREDENTIAL_FLAGS] + list(_MCP_SAVED_SETTINGS)
|
|
111
|
+
# os.environ rather than get_env_var: model defaults must not be persisted
|
|
112
|
+
# as if the user had chosen them.
|
|
113
|
+
return [key for key in keys if os.environ.get(f"{EnvConfig.PREFIX}{key}")]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _save_mcp_credentials(keys: List[str]) -> None:
|
|
117
|
+
"""Persist the resolved credentials to the user-level ``.env``.
|
|
118
|
+
|
|
119
|
+
Best effort: a failure here is reported but never takes the MCP connection
|
|
120
|
+
down with it. Messages go to stderr — stdout is the JSON-RPC channel.
|
|
121
|
+
"""
|
|
122
|
+
from dataquery.config import EnvConfig
|
|
123
|
+
|
|
124
|
+
if not any(key in keys for key in ("CLIENT_ID", "CLIENT_SECRET", "BEARER_TOKEN")):
|
|
125
|
+
print(
|
|
126
|
+
"--save-credentials: nothing to save; pass --client-id/--client-secret "
|
|
127
|
+
"or set DATAQUERY_CLIENT_ID/DATAQUERY_CLIENT_SECRET.",
|
|
128
|
+
file=sys.stderr,
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
values = {key: os.environ.get(f"{EnvConfig.PREFIX}{key}") for key in keys}
|
|
133
|
+
try:
|
|
134
|
+
env_file = EnvConfig.save_user_env(values)
|
|
135
|
+
except OSError as exc:
|
|
136
|
+
print(f"--save-credentials: could not write credentials: {exc}", file=sys.stderr)
|
|
137
|
+
return
|
|
138
|
+
saved = ", ".join(f"{EnvConfig.PREFIX}{key}" for key in keys)
|
|
139
|
+
print(f"Saved {saved} to {env_file} (owner-only)", file=sys.stderr)
|
|
140
|
+
|
|
141
|
+
|
|
85
142
|
def create_parser() -> argparse.ArgumentParser:
|
|
86
143
|
"""Create the top-level CLI parser with subcommands."""
|
|
87
144
|
parser = argparse.ArgumentParser(description="Command Line Interface for the DataQuery SDK")
|
|
@@ -204,8 +261,6 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
204
261
|
p_fn.add_argument("--list", action="store_true", help="List all available functions")
|
|
205
262
|
p_fn.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
206
263
|
|
|
207
|
-
# ── DataQuery API v2 endpoints (skill-facing surface) ────────────────
|
|
208
|
-
|
|
209
264
|
def _ts_args(p: argparse.ArgumentParser) -> None:
|
|
210
265
|
p.add_argument("--data", choices=["REFERENCE_DATA", "NO_REFERENCE_DATA", "ALL"], default=None)
|
|
211
266
|
p.add_argument("--start-date", help="YYYYMMDD or TODAY-Nx (x=D/W/M/Y). Default: TODAY-1D")
|
|
@@ -298,12 +353,35 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
298
353
|
"Bridge a desktop MCP client (stdio) to a remote streamable-HTTP MCP\n"
|
|
299
354
|
"server, authenticating with an OAuth client-credentials (AuthE) token\n"
|
|
300
355
|
"minted from the DATAQUERY_* environment. Point your MCP client's\n"
|
|
301
|
-
"`command` at: dataquery mcp-connect --url <MCP_URL
|
|
356
|
+
"`command` at: dataquery mcp-connect --url <MCP_URL>\n"
|
|
357
|
+
"\n"
|
|
358
|
+
"Credentials passed as flags are exported into the DATAQUERY_*\n"
|
|
359
|
+
"environment of this process; add --save-credentials to also write\n"
|
|
360
|
+
"them to ~/.dataquery/.env, which every later SDK call picks up so\n"
|
|
361
|
+
"no environment has to be set up again."
|
|
302
362
|
),
|
|
303
363
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
304
364
|
)
|
|
305
365
|
p_connect.add_argument("--url", required=True, help="Remote MCP endpoint URL")
|
|
306
366
|
p_connect.add_argument("--name", default="dataquery-mcp", help="Proxy server name (default: dataquery-mcp)")
|
|
367
|
+
p_connect.add_argument("--client-id", default=None, help="OAuth client ID (exported as DATAQUERY_CLIENT_ID)")
|
|
368
|
+
p_connect.add_argument(
|
|
369
|
+
"--client-secret",
|
|
370
|
+
default=None,
|
|
371
|
+
help="OAuth client secret (exported as DATAQUERY_CLIENT_SECRET); visible in the process list, "
|
|
372
|
+
"so prefer DATAQUERY_CLIENT_SECRET or --env-file on shared machines",
|
|
373
|
+
)
|
|
374
|
+
p_connect.add_argument(
|
|
375
|
+
"--bearer-token",
|
|
376
|
+
default=None,
|
|
377
|
+
help="Bearer token to use instead of OAuth credentials (exported as DATAQUERY_BEARER_TOKEN)",
|
|
378
|
+
)
|
|
379
|
+
p_connect.add_argument(
|
|
380
|
+
"--save-credentials",
|
|
381
|
+
action="store_true",
|
|
382
|
+
help="Also save the resolved credentials to ~/.dataquery/.env (owner-only) so later SDK use "
|
|
383
|
+
"needs no environment variables",
|
|
384
|
+
)
|
|
307
385
|
|
|
308
386
|
return parser
|
|
309
387
|
|
|
@@ -518,9 +596,6 @@ async def cmd_search(args: argparse.Namespace) -> int:
|
|
|
518
596
|
return 0
|
|
519
597
|
|
|
520
598
|
|
|
521
|
-
# ── DataQuery API v2 command handlers ────────────────────────────────────
|
|
522
|
-
|
|
523
|
-
|
|
524
599
|
async def cmd_groups_search(args: argparse.Namespace) -> int:
|
|
525
600
|
async with DataQuery(args.env_file) as dq:
|
|
526
601
|
items = await dq.search_groups_async(args.keywords, page=args.page)
|
|
@@ -732,14 +807,7 @@ def cmd_function_help(args: argparse.Namespace) -> int:
|
|
|
732
807
|
|
|
733
808
|
|
|
734
809
|
async def cmd_mcp_connect(args: argparse.Namespace) -> int:
|
|
735
|
-
"""Bridge a desktop MCP client (stdio) to a remote streamable-HTTP MCP server.
|
|
736
|
-
|
|
737
|
-
Mints an OAuth client-credentials (AuthE) token with the SDK's own
|
|
738
|
-
TokenManager from the ``DATAQUERY_*`` environment and attaches a fresh bearer
|
|
739
|
-
token to every upstream request. Launched by an MCP client as its stdio
|
|
740
|
-
``command``; stdout carries only the MCP JSON-RPC stream, so all diagnostics
|
|
741
|
-
go to stderr.
|
|
742
|
-
"""
|
|
810
|
+
"""Bridge a desktop MCP client (stdio) to a remote streamable-HTTP MCP server."""
|
|
743
811
|
try:
|
|
744
812
|
import httpx
|
|
745
813
|
from fastmcp import FastMCP
|
|
@@ -757,14 +825,24 @@ async def cmd_mcp_connect(args: argparse.Namespace) -> int:
|
|
|
757
825
|
from dataquery.config import EnvConfig
|
|
758
826
|
from dataquery.transport.auth import TokenManager
|
|
759
827
|
|
|
760
|
-
|
|
828
|
+
if getattr(args, "env_file", None):
|
|
829
|
+
EnvConfig.load_env_file(Path(args.env_file))
|
|
830
|
+
# Load saved credentials before exporting, so they too land in the process
|
|
831
|
+
# env and a re-launch with --save-credentials rewrites them instead of
|
|
832
|
+
# reporting nothing to save. Loaded last, so it never wins over the above.
|
|
833
|
+
EnvConfig.load_user_env_file()
|
|
834
|
+
savable = _export_mcp_credentials(args)
|
|
835
|
+
if getattr(args, "save_credentials", False):
|
|
836
|
+
_save_mcp_credentials(savable)
|
|
837
|
+
|
|
838
|
+
config = EnvConfig.create_client_config()
|
|
761
839
|
token_manager = TokenManager(config)
|
|
762
840
|
|
|
763
841
|
class _AutheAuth(httpx.Auth):
|
|
764
842
|
"""Stamp a fresh AuthE bearer token (from the SDK TokenManager) per request."""
|
|
765
843
|
|
|
766
844
|
async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
|
|
767
|
-
header = await token_manager.get_valid_token()
|
|
845
|
+
header = await token_manager.get_valid_token()
|
|
768
846
|
if not header:
|
|
769
847
|
raise DataQueryError(
|
|
770
848
|
"Could not obtain an OAuth token \u2014 check DATAQUERY_CLIENT_ID, "
|
|
@@ -776,7 +854,6 @@ async def cmd_mcp_connect(args: argparse.Namespace) -> int:
|
|
|
776
854
|
|
|
777
855
|
transport = StreamableHttpTransport(args.url, auth=_AutheAuth())
|
|
778
856
|
proxy = FastMCP.as_proxy(transport, name=args.name)
|
|
779
|
-
# stdout is reserved for the MCP JSON-RPC stream; keep the banner off it.
|
|
780
857
|
await proxy.run_async(transport="stdio", show_banner=False)
|
|
781
858
|
return 0
|
|
782
859
|
|
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Environment-based configuration for the DATAQUERY SDK.
|
|
3
|
-
|
|
4
|
-
Loads settings from environment variables (or a ``.env`` file) into a
|
|
5
|
-
:class:`ClientConfig` Pydantic model. Field declarations live on
|
|
6
|
-
``ClientConfig`` — this module only handles the env-var → field projection,
|
|
7
|
-
type coercion, and cross-field validation.
|
|
8
|
-
|
|
9
|
-
Single source of truth: :data:`ClientConfig.model_fields`. The
|
|
10
|
-
``DEFAULTS`` table here is derived from the model at import time so the two
|
|
11
|
-
cannot drift.
|
|
12
|
-
"""
|
|
1
|
+
"""Environment-based configuration for the DATAQUERY SDK."""
|
|
13
2
|
|
|
14
3
|
from __future__ import annotations
|
|
15
4
|
|
|
16
5
|
import os
|
|
6
|
+
import re
|
|
17
7
|
from pathlib import Path
|
|
18
|
-
from typing import Any, Dict, Optional, Union, get_args, get_origin
|
|
8
|
+
from typing import Any, Dict, List, Mapping, Optional, Union, get_args, get_origin
|
|
19
9
|
|
|
20
10
|
from dotenv import load_dotenv
|
|
21
11
|
from pydantic_core import PydanticUndefined
|
|
@@ -41,6 +31,37 @@ _SENSITIVE_FIELDS = frozenset(
|
|
|
41
31
|
}
|
|
42
32
|
)
|
|
43
33
|
|
|
34
|
+
# User-level config: credentials saved once (e.g. by `dataquery mcp-connect
|
|
35
|
+
# --save-credentials`) and reused by every later SDK call in any process.
|
|
36
|
+
_USER_CONFIG_DIR_ENV = "DATAQUERY_CONFIG_DIR"
|
|
37
|
+
_USER_CONFIG_DIR_DEFAULT = "~/.dataquery"
|
|
38
|
+
_USER_ENV_FILENAME = ".env"
|
|
39
|
+
|
|
40
|
+
_USER_ENV_HEADER = [
|
|
41
|
+
"# DataQuery SDK credentials.",
|
|
42
|
+
"# Written by `dataquery mcp-connect --save-credentials`; loaded as a",
|
|
43
|
+
"# fallback only, so shell env vars and a local .env still win.",
|
|
44
|
+
"# Values are read literally: ${VAR} is not expanded here.",
|
|
45
|
+
"",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
_ENV_LINE_RE = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=")
|
|
49
|
+
|
|
50
|
+
# Characters common in ids, secrets and URLs that need no .env quoting.
|
|
51
|
+
_UNQUOTED_EXTRA_CHARS = "._-:/@+~"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _format_env_value(value: str) -> str:
|
|
55
|
+
"""Render a value for a ``.env`` line, quoting only when it needs it."""
|
|
56
|
+
if value and all(ch.isalnum() or ch in _UNQUOTED_EXTRA_CHARS for ch in value):
|
|
57
|
+
return value
|
|
58
|
+
if "'" not in value and "\n" not in value and "\r" not in value:
|
|
59
|
+
return f"'{value}'"
|
|
60
|
+
# Escapes keep the entry on a single physical line, which the line-based
|
|
61
|
+
# merge in ``save_user_env`` depends on.
|
|
62
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r")
|
|
63
|
+
return f'"{escaped}"'
|
|
64
|
+
|
|
44
65
|
|
|
45
66
|
def _env_name_for(field_name: str) -> str:
|
|
46
67
|
"""Project a Pydantic field name to its env-var key (no PREFIX)."""
|
|
@@ -57,12 +78,7 @@ def _unwrap_optional(annotation: Any) -> Any:
|
|
|
57
78
|
|
|
58
79
|
|
|
59
80
|
def _build_defaults() -> Dict[str, Optional[str]]:
|
|
60
|
-
"""Compute the ``EnvConfig.DEFAULTS`` table from the model.
|
|
61
|
-
|
|
62
|
-
Booleans become ``"true"``/``"false"`` (lowercase, matching the
|
|
63
|
-
tokens :meth:`EnvConfig.get_bool` accepts). Numeric / string defaults
|
|
64
|
-
are stringified directly. ``None`` defaults stay ``None``.
|
|
65
|
-
"""
|
|
81
|
+
"""Compute the ``EnvConfig.DEFAULTS`` table from the model."""
|
|
66
82
|
defaults: Dict[str, Optional[str]] = {}
|
|
67
83
|
for field_name, field in ClientConfig.model_fields.items():
|
|
68
84
|
env_key = _env_name_for(field_name)
|
|
@@ -79,12 +95,7 @@ def _build_defaults() -> Dict[str, Optional[str]]:
|
|
|
79
95
|
|
|
80
96
|
|
|
81
97
|
class EnvConfig:
|
|
82
|
-
"""Environment-based configuration loader for the DataQuery SDK.
|
|
83
|
-
|
|
84
|
-
All methods are class methods — this class is a namespace, never
|
|
85
|
-
instantiated. Field definitions, types, and defaults all originate from
|
|
86
|
-
:class:`dataquery.types.models.ClientConfig`.
|
|
87
|
-
"""
|
|
98
|
+
"""Environment-based configuration loader for the DataQuery SDK."""
|
|
88
99
|
|
|
89
100
|
PREFIX = "DATAQUERY_"
|
|
90
101
|
DEFAULTS: Dict[str, Optional[str]] = _build_defaults()
|
|
@@ -97,6 +108,77 @@ class EnvConfig:
|
|
|
97
108
|
if env_file.exists():
|
|
98
109
|
load_dotenv(env_file)
|
|
99
110
|
|
|
111
|
+
@classmethod
|
|
112
|
+
def user_config_dir(cls) -> Path:
|
|
113
|
+
"""Directory holding user-level SDK config (``$DATAQUERY_CONFIG_DIR`` or ``~/.dataquery``)."""
|
|
114
|
+
raw = os.getenv(_USER_CONFIG_DIR_ENV) or _USER_CONFIG_DIR_DEFAULT
|
|
115
|
+
return Path(raw).expanduser()
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def user_env_file(cls) -> Path:
|
|
119
|
+
"""Path of the user-level ``.env`` holding saved credentials."""
|
|
120
|
+
return cls.user_config_dir() / _USER_ENV_FILENAME
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def load_user_env_file(cls) -> bool:
|
|
124
|
+
"""Load the user-level ``.env`` as a fallback; never overrides what is already set."""
|
|
125
|
+
env_file = cls.user_env_file()
|
|
126
|
+
if not env_file.exists():
|
|
127
|
+
return False
|
|
128
|
+
# interpolate=False: values here are literal secrets, and dotenv's
|
|
129
|
+
# ${VAR} expansion runs after unquoting, so no quoting style escapes it.
|
|
130
|
+
load_dotenv(env_file, override=False, interpolate=False)
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def save_user_env(cls, values: Mapping[str, Optional[str]]) -> Path:
|
|
135
|
+
"""Merge ``DATAQUERY_<key>=<value>`` pairs into the user-level ``.env``.
|
|
136
|
+
|
|
137
|
+
Keys are unprefixed (``CLIENT_ID``) and empty values are skipped. Lines
|
|
138
|
+
for a key being saved are rewritten in place, anything else already in
|
|
139
|
+
the file is preserved, and the file is (re)written owner-readable only.
|
|
140
|
+
"""
|
|
141
|
+
updates: Dict[str, str] = {}
|
|
142
|
+
for key, value in values.items():
|
|
143
|
+
if value:
|
|
144
|
+
updates[f"{cls.PREFIX}{key}"] = value
|
|
145
|
+
|
|
146
|
+
env_file = cls.user_env_file()
|
|
147
|
+
env_file.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
try:
|
|
149
|
+
env_file.parent.chmod(0o700)
|
|
150
|
+
except OSError: # pragma: no cover - non-POSIX filesystem
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
lines: List[str] = []
|
|
154
|
+
written: set[str] = set()
|
|
155
|
+
if env_file.exists():
|
|
156
|
+
for line in env_file.read_text().splitlines():
|
|
157
|
+
match = _ENV_LINE_RE.match(line)
|
|
158
|
+
line_key = match.group(1) if match else None
|
|
159
|
+
if line_key is None or line_key not in updates:
|
|
160
|
+
lines.append(line)
|
|
161
|
+
elif line_key not in written:
|
|
162
|
+
lines.append(f"{line_key}={_format_env_value(updates[line_key])}")
|
|
163
|
+
written.add(line_key)
|
|
164
|
+
# else: drop a stale duplicate of a key just rewritten above
|
|
165
|
+
else:
|
|
166
|
+
lines.extend(_USER_ENV_HEADER)
|
|
167
|
+
|
|
168
|
+
for key, value in updates.items():
|
|
169
|
+
if key not in written:
|
|
170
|
+
lines.append(f"{key}={_format_env_value(value)}")
|
|
171
|
+
|
|
172
|
+
# O_CREAT mode only applies to new files, so chmod covers the rest.
|
|
173
|
+
fd = os.open(env_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
174
|
+
with os.fdopen(fd, "w") as handle:
|
|
175
|
+
handle.write("\n".join(lines) + "\n")
|
|
176
|
+
try:
|
|
177
|
+
env_file.chmod(0o600)
|
|
178
|
+
except OSError: # pragma: no cover - non-POSIX filesystem
|
|
179
|
+
pass
|
|
180
|
+
return env_file
|
|
181
|
+
|
|
100
182
|
@classmethod
|
|
101
183
|
def get_env_var(cls, key: str, default: Optional[str] = None) -> Optional[str]:
|
|
102
184
|
"""Read ``$DATAQUERY_<key>`` with the model default as the fallback."""
|
|
@@ -146,17 +228,18 @@ class EnvConfig:
|
|
|
146
228
|
config_data: Optional[Dict[str, Any]] = None,
|
|
147
229
|
env_file: Optional[Path] = None,
|
|
148
230
|
) -> ClientConfig:
|
|
149
|
-
"""Build a :class:`ClientConfig` from env vars or an explicit dict.
|
|
150
|
-
|
|
151
|
-
When ``config_data`` is provided the env-var path is skipped entirely
|
|
152
|
-
— the dict is passed straight to :class:`ClientConfig`.
|
|
153
|
-
"""
|
|
231
|
+
"""Build a :class:`ClientConfig` from env vars or an explicit dict."""
|
|
154
232
|
if config_data is not None:
|
|
155
233
|
return ClientConfig(**config_data)
|
|
156
234
|
|
|
157
235
|
if env_file is not None:
|
|
158
236
|
cls.load_env_file(env_file)
|
|
159
237
|
|
|
238
|
+
# Lowest precedence, loaded last so it fills only what is still unset:
|
|
239
|
+
# credentials saved by `mcp-connect --save-credentials`, which let a
|
|
240
|
+
# later DataQuery() authenticate with no environment of its own.
|
|
241
|
+
cls.load_user_env_file()
|
|
242
|
+
|
|
160
243
|
if not cls.get_env_var("BASE_URL"):
|
|
161
244
|
raise ConfigurationError(f"{cls.PREFIX}BASE_URL environment variable is required")
|
|
162
245
|
|
|
@@ -275,11 +358,7 @@ class EnvConfig:
|
|
|
275
358
|
|
|
276
359
|
@classmethod
|
|
277
360
|
def create_env_template(cls, output_path: Optional[Path] = None) -> Path:
|
|
278
|
-
"""Write a ``.env`` template listing every supported variable.
|
|
279
|
-
|
|
280
|
-
Lines are auto-generated from :data:`ClientConfig.model_fields` so a
|
|
281
|
-
new field shows up here automatically (no separate maintenance).
|
|
282
|
-
"""
|
|
361
|
+
"""Write a ``.env`` template listing every supported variable."""
|
|
283
362
|
if output_path is None:
|
|
284
363
|
output_path = Path(".env.template")
|
|
285
364
|
if not isinstance(output_path, Path):
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Enhanced logging configuration for the DATAQUERY SDK.
|
|
3
|
-
|
|
4
|
-
Provides structured logging, performance metrics, request/response logging,
|
|
5
|
-
and configurable log levels and formats.
|
|
6
|
-
"""
|
|
1
|
+
"""Enhanced logging configuration for the DATAQUERY SDK."""
|
|
7
2
|
|
|
8
3
|
import logging
|
|
9
4
|
from dataclasses import dataclass, field
|
|
@@ -353,21 +348,7 @@ def create_logging_config(
|
|
|
353
348
|
enable_request_logging: bool = False,
|
|
354
349
|
enable_performance_logging: bool = True,
|
|
355
350
|
) -> LoggingConfig:
|
|
356
|
-
"""
|
|
357
|
-
Create logging configuration.
|
|
358
|
-
|
|
359
|
-
Args:
|
|
360
|
-
level: Log level
|
|
361
|
-
format: Log format
|
|
362
|
-
enable_console: Whether to enable console logging
|
|
363
|
-
enable_file: Whether to enable file logging
|
|
364
|
-
log_file: Path to log file
|
|
365
|
-
enable_request_logging: Whether to log HTTP requests/responses
|
|
366
|
-
enable_performance_logging: Whether to log performance metrics
|
|
367
|
-
|
|
368
|
-
Returns:
|
|
369
|
-
Logging configuration
|
|
370
|
-
"""
|
|
351
|
+
"""Create logging configuration."""
|
|
371
352
|
return LoggingConfig(
|
|
372
353
|
level=level,
|
|
373
354
|
format=format,
|
|
@@ -380,13 +361,5 @@ def create_logging_config(
|
|
|
380
361
|
|
|
381
362
|
|
|
382
363
|
def create_logging_manager(config: LoggingConfig) -> LoggingManager:
|
|
383
|
-
"""
|
|
384
|
-
Create a logging manager with the specified configuration.
|
|
385
|
-
|
|
386
|
-
Args:
|
|
387
|
-
config: Logging configuration
|
|
388
|
-
|
|
389
|
-
Returns:
|
|
390
|
-
Configured logging manager
|
|
391
|
-
"""
|
|
364
|
+
"""Create a logging manager with the specified configuration."""
|
|
392
365
|
return LoggingManager(config)
|
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
"""Internal tunable constants used across the SDK.
|
|
2
|
-
|
|
3
|
-
Centralizing these values here avoids drift between code paths that must agree
|
|
4
|
-
(e.g. the single-stream and parallel downloaders share thresholds) and gives a
|
|
5
|
-
single place to adjust tunables. Pydantic model field defaults and validation
|
|
6
|
-
bounds are intentionally NOT hoisted here — those are part of the public
|
|
7
|
-
schema.
|
|
8
|
-
|
|
9
|
-
Constants are organized into topical submodules; this ``__init__`` re-exports
|
|
10
|
-
them so existing call sites using ``from . import constants as C`` keep
|
|
11
|
-
working without change. New code may also import from a specific submodule,
|
|
12
|
-
e.g. ``from dataquery.constants.api import API_GROUPS``.
|
|
13
|
-
"""
|
|
1
|
+
"""Internal tunable constants used across the SDK."""
|
|
14
2
|
|
|
15
3
|
from __future__ import annotations
|
|
16
4
|
|
|
@@ -26,15 +26,6 @@ TEMP_SUFFIX = ".part"
|
|
|
26
26
|
MBPS_TO_BYTES_PER_SECOND = 125_000
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
# Default size, in days, of each chunk when splitting a date range so that
|
|
30
|
-
# the available-files endpoint is queried over a smaller window per call.
|
|
31
|
-
# Used by ``split_date_range_into_chunks``. The endpoint caps each call at
|
|
32
|
-
# one calendar month; smaller windows just keep responses lighter. (The old
|
|
33
|
-
# group-level limits, e.g. 14 days for RESEARCH_EQUITY_ALL, were lifted
|
|
34
|
-
# server-side.)
|
|
35
29
|
DEFAULT_WRITTEN_RESEARCH_CHUNK_DAYS: int = 7
|
|
36
30
|
|
|
37
|
-
# Error string set on the OperationReport when the available-files endpoint
|
|
38
|
-
# returns nothing for a date window. Chunked workflows match on it to tell
|
|
39
|
-
# a quiet window apart from a real failure.
|
|
40
31
|
NO_FILES_FOUND_ERROR: str = "No available files found for date range"
|