dataquery-sdk 1.2.3__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.3 → dataquery_sdk-1.2.4}/CHANGELOG.md +4 -2
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/PKG-INFO +4 -4
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/__init__.py +1 -1
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/cli.py +94 -2
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/config/env.py +109 -1
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/core/client.py +1 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/sse/client.py +1 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/types/models.py +11 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/utils.py +3 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/PKG-INFO +4 -4
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/requires.txt +3 -3
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/pyproject.toml +4 -4
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/CONTRIBUTING.md +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/LICENSE +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/MANIFEST.in +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/README.md +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/config/__init__.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/config/logging.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/constants/__init__.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/constants/api.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/constants/download.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/constants/rate_limit.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/constants/sse.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/core/__init__.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/core/_mixins.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/core/_sync.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/data/function.json +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/dataquery.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/download/__init__.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/download/parallel.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/download/utils.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/export.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/function_registry.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/mcp_proxy.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/py.typed +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/sse/__init__.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/sse/event_store.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/sse/subscriber.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/transport/__init__.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/transport/auth.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/transport/connection_pool.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/transport/rate_limiter.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/transport/retry.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/types/__init__.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery/types/exceptions.py +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/SOURCES.txt +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/dependency_links.txt +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/entry_points.txt +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/dataquery_sdk.egg-info/top_level.txt +0 -0
- {dataquery_sdk-1.2.3 → dataquery_sdk-1.2.4}/setup.cfg +0 -0
|
@@ -51,5 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
51
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
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
53
|
- Exports: `Paginated`, `Link`, `APIResponseError` are now part of the public API
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
@@ -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
|
|
@@ -80,6 +81,64 @@ def _print_error(message: str, *, suggestion: Optional[str] = None) -> None:
|
|
|
80
81
|
print(json.dumps(envelope, indent=2))
|
|
81
82
|
|
|
82
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
|
+
|
|
83
142
|
def create_parser() -> argparse.ArgumentParser:
|
|
84
143
|
"""Create the top-level CLI parser with subcommands."""
|
|
85
144
|
parser = argparse.ArgumentParser(description="Command Line Interface for the DataQuery SDK")
|
|
@@ -294,12 +353,35 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
294
353
|
"Bridge a desktop MCP client (stdio) to a remote streamable-HTTP MCP\n"
|
|
295
354
|
"server, authenticating with an OAuth client-credentials (AuthE) token\n"
|
|
296
355
|
"minted from the DATAQUERY_* environment. Point your MCP client's\n"
|
|
297
|
-
"`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."
|
|
298
362
|
),
|
|
299
363
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
300
364
|
)
|
|
301
365
|
p_connect.add_argument("--url", required=True, help="Remote MCP endpoint URL")
|
|
302
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
|
+
)
|
|
303
385
|
|
|
304
386
|
return parser
|
|
305
387
|
|
|
@@ -743,7 +825,17 @@ async def cmd_mcp_connect(args: argparse.Namespace) -> int:
|
|
|
743
825
|
from dataquery.config import EnvConfig
|
|
744
826
|
from dataquery.transport.auth import TokenManager
|
|
745
827
|
|
|
746
|
-
|
|
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()
|
|
747
839
|
token_manager = TokenManager(config)
|
|
748
840
|
|
|
749
841
|
class _AutheAuth(httpx.Auth):
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
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
|
|
8
9
|
|
|
9
10
|
from dotenv import load_dotenv
|
|
10
11
|
from pydantic_core import PydanticUndefined
|
|
@@ -30,6 +31,37 @@ _SENSITIVE_FIELDS = frozenset(
|
|
|
30
31
|
}
|
|
31
32
|
)
|
|
32
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
|
+
|
|
33
65
|
|
|
34
66
|
def _env_name_for(field_name: str) -> str:
|
|
35
67
|
"""Project a Pydantic field name to its env-var key (no PREFIX)."""
|
|
@@ -76,6 +108,77 @@ class EnvConfig:
|
|
|
76
108
|
if env_file.exists():
|
|
77
109
|
load_dotenv(env_file)
|
|
78
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
|
+
|
|
79
182
|
@classmethod
|
|
80
183
|
def get_env_var(cls, key: str, default: Optional[str] = None) -> Optional[str]:
|
|
81
184
|
"""Read ``$DATAQUERY_<key>`` with the model default as the fallback."""
|
|
@@ -132,6 +235,11 @@ class EnvConfig:
|
|
|
132
235
|
if env_file is not None:
|
|
133
236
|
cls.load_env_file(env_file)
|
|
134
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
|
+
|
|
135
243
|
if not cls.get_env_var("BASE_URL"):
|
|
136
244
|
raise ConfigurationError(f"{cls.PREFIX}BASE_URL environment variable is required")
|
|
137
245
|
|
|
@@ -157,6 +157,7 @@ class SSEClient:
|
|
|
157
157
|
async def _get_headers(self) -> dict:
|
|
158
158
|
headers = await self.auth_manager.get_headers()
|
|
159
159
|
headers["Accept"] = "text/event-stream"
|
|
160
|
+
headers.update(self.config.get_custom_headers())
|
|
160
161
|
if self._last_event_id is not None:
|
|
161
162
|
headers["Last-Event-ID"] = self._last_event_id
|
|
162
163
|
return headers
|
|
@@ -51,6 +51,10 @@ class ClientConfig(BaseModel):
|
|
|
51
51
|
)
|
|
52
52
|
context_path: Optional[str] = Field(default="/research/dataquery-authe/api/v2", description="API context path")
|
|
53
53
|
api_version: str = Field(default="2.0.0", description="API version")
|
|
54
|
+
x_user_agent: Optional[str] = Field(
|
|
55
|
+
default=None,
|
|
56
|
+
description="Optional value for the X-User-Agent header sent on each API request",
|
|
57
|
+
)
|
|
54
58
|
files_base_url: Optional[str] = Field(
|
|
55
59
|
default="https://api-dataquery.jpmchase.com",
|
|
56
60
|
description="Separate base URL for file endpoints",
|
|
@@ -242,6 +246,13 @@ class ClientConfig(BaseModel):
|
|
|
242
246
|
kwargs["proxy_headers"] = {"Proxy-Authorization": f"Basic {token}"}
|
|
243
247
|
return kwargs
|
|
244
248
|
|
|
249
|
+
def get_custom_headers(self) -> Dict[str, str]:
|
|
250
|
+
"""Return extra headers to attach to every API request (e.g. X-User-Agent)."""
|
|
251
|
+
headers: Dict[str, str] = {}
|
|
252
|
+
if self.x_user_agent:
|
|
253
|
+
headers["X-User-Agent"] = self.x_user_agent
|
|
254
|
+
return headers
|
|
255
|
+
|
|
245
256
|
@property
|
|
246
257
|
def api_base_url(self) -> str:
|
|
247
258
|
"""Get the complete API base URL including context path."""
|
|
@@ -171,6 +171,9 @@ DATAQUERY_DEFAULT_DIR=files
|
|
|
171
171
|
# Default: DATAQUERY-SDK/1.2.1
|
|
172
172
|
DATAQUERY_USER_AGENT=DATAQUERY-SDK/1.2.1
|
|
173
173
|
|
|
174
|
+
# Optional custom X-User-Agent header sent on every API request
|
|
175
|
+
# DATAQUERY_X_USER_AGENT=MyApp/1.0
|
|
176
|
+
|
|
174
177
|
# Enable HTTP/2 support (true/false)
|
|
175
178
|
# Default: true
|
|
176
179
|
DATAQUERY_ENABLE_HTTP2=true
|
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
aiohttp<4.0.0,>=3.
|
|
1
|
+
aiohttp<4.0.0,>=3.14.3
|
|
2
2
|
pydantic<3.0.0,>=2.0.0
|
|
3
3
|
structlog>=23.0.0
|
|
4
4
|
python-dotenv>=1.0.0
|
|
@@ -20,7 +20,7 @@ fastmcp>=2.14
|
|
|
20
20
|
pip-audit>=2.7.0
|
|
21
21
|
build>=1.3.0
|
|
22
22
|
urllib3>=2.7.0
|
|
23
|
-
pymdown-extensions>=
|
|
23
|
+
pymdown-extensions>=11.0.0
|
|
24
24
|
|
|
25
25
|
[dev]
|
|
26
26
|
pytest>=7.0.0
|
|
@@ -39,7 +39,7 @@ mkdocs>=1.5.0
|
|
|
39
39
|
mkdocs-material>=9.0.0
|
|
40
40
|
mkdocstrings[python]>=0.24.0
|
|
41
41
|
mike>=1.1.0
|
|
42
|
-
pymdown-extensions>=
|
|
42
|
+
pymdown-extensions>=11.0.0
|
|
43
43
|
|
|
44
44
|
[mcp]
|
|
45
45
|
fastmcp>=2.14
|
|
@@ -27,7 +27,7 @@ classifiers = [
|
|
|
27
27
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
28
28
|
]
|
|
29
29
|
dependencies = [
|
|
30
|
-
"aiohttp>=3.
|
|
30
|
+
"aiohttp>=3.14.3,<4.0.0",
|
|
31
31
|
"pydantic>=2.0.0,<3.0.0",
|
|
32
32
|
"structlog>=23.0.0",
|
|
33
33
|
"python-dotenv>=1.0.0",
|
|
@@ -71,7 +71,7 @@ docs = [
|
|
|
71
71
|
"mkdocs-material>=9.0.0",
|
|
72
72
|
"mkdocstrings[python]>=0.24.0",
|
|
73
73
|
"mike>=1.1.0",
|
|
74
|
-
"pymdown-extensions>=
|
|
74
|
+
"pymdown-extensions>=11.0.0",
|
|
75
75
|
]
|
|
76
76
|
pandas = [
|
|
77
77
|
"pandas>=2.0.0",
|
|
@@ -95,7 +95,7 @@ all = [
|
|
|
95
95
|
"pip-audit>=2.7.0",
|
|
96
96
|
"build>=1.3.0",
|
|
97
97
|
"urllib3>=2.7.0",
|
|
98
|
-
"pymdown-extensions>=
|
|
98
|
+
"pymdown-extensions>=11.0.0",
|
|
99
99
|
]
|
|
100
100
|
|
|
101
101
|
[project.scripts]
|
|
@@ -210,5 +210,5 @@ dev = [
|
|
|
210
210
|
"mypy>=1.17.1",
|
|
211
211
|
"pytest>=8.4.2",
|
|
212
212
|
"urllib3>=2.7.0",
|
|
213
|
-
"pymdown-extensions>=
|
|
213
|
+
"pymdown-extensions>=11.0.0",
|
|
214
214
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|