quantjourney-api 0.1.1__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,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: quantjourney-api
3
+ Version: 0.1.1
4
+ Summary: Python SDK for QuantJourney qj-api product contracts.
5
+ Author: QuantJourney
6
+ License-Expression: LicenseRef-Proprietary
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: quantjourney-common-sdk<0.2,>=0.1.1
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
12
+ Provides-Extra: frames
13
+ Requires-Dist: pandas>=2.0.0; extra == "frames"
14
+
15
+ # quantjourney-api
16
+
17
+ Python SDK for the public/product `qj-api` contract.
18
+
19
+ Canonical package metadata:
20
+
21
+ | Field | Value |
22
+ | --- | --- |
23
+ | Repository | `_repo_qj_api_sdk` |
24
+ | Distribution | `quantjourney-api` |
25
+ | Python import | `qj.api` |
26
+ | Runtime client | `QJ` |
27
+ | Backend boundary | `qj-api` / `https://api.quantjourney.cloud` |
28
+ | Version | `0.1.1` |
29
+ | Git tag | `api-v0.1.1` |
30
+
31
+ `quantjourney-api` is the public product/API SDK. The `quantjourney` umbrella
32
+ package lives in `packages/quantjourney` and installs `quantjourney-api`.
33
+
34
+ ```python
35
+ from qj.api import QJ, QuantJourneyAPI
36
+
37
+ qj = QJ.from_env()
38
+ health = qj.health()
39
+ capabilities = qj.capabilities()
40
+
41
+ # Provider connectors through qj-api
42
+ profile = qj.fmp.get_company_profile(symbol="NVDA")
43
+ gdp = qj.fred.get_series(series_id="GDP")
44
+ filings = qj.sec.get_recent_filings(limit=5)
45
+
46
+ # Provider-agnostic domain routes
47
+ routes = qj.domains.list()
48
+ prices = qj.domains.equity.pricing.get_historical_prices(symbol="NVDA")
49
+
50
+ # Product API surfaces
51
+ analytics = qj.analytics.registry()
52
+ ratios = qj.ratios.available(source="eod")
53
+ indicators = qj.technical.available()
54
+
55
+ # Compatibility aliases
56
+ qj2 = QuantJourneyAPI.from_env()
57
+ ```
58
+
59
+ Local smoke from this checkout:
60
+
61
+ ```bash
62
+ python scripts/qj_api_smoke.py --env .env
63
+ python scripts/endpoint_matrix.py --env .env
64
+ python scripts/endpoint_matrix.py --env .env --list
65
+ python scripts/endpoint_matrix.py --env .env --group connectors --allow-failures
66
+ python scripts/endpoint_matrix.py --env .env --group connector_examples --include-connector-examples --allow-failures
67
+ python scripts/connector_matrix.py --env .env
68
+ ```
69
+
70
+ `scripts/connectors.json` stores one smoke example per connector. The endpoint
71
+ matrix verifies every connector route; `connector_matrix.py` runs the examples
72
+ and prints a compact result table.
73
+
74
+ Environment variables:
75
+
76
+ | Variable | Purpose |
77
+ | --- | --- |
78
+ | `QJ_API_URL` | qj-api base URL, default `https://api.quantjourney.cloud` |
79
+ | `QJ_API_TOKEN` | bearer token |
80
+ | `QJ_AUTH_URL` | qj-auth base URL for token exchange |
81
+ | `QJ_API_KEY` | `QJ_live_*` API key used to mint `QJ_API_TOKEN` |
82
+ | `QJ_API_CLIENT_ID` | service account client id for `client_credentials` |
83
+ | `QJ_API_CLIENT_SECRET` | `QJ_svc_*` service secret for `client_credentials` |
84
+ | `QJ_API_TENANT_ID` | optional tenant projection |
85
+ | `QJ_API_TEAM_ID` | optional team projection |
86
+
87
+ Install from PyPI:
88
+
89
+ ```bash
90
+ pip install quantjourney-api==0.1.1
91
+ ```
92
+
93
+ `quantjourney-common-sdk` is installed automatically as a dependency. Local
94
+ development can still use editable path installs for sibling checkouts.
95
+
96
+ The new surface keeps the smaller `qj.api` namespace, uses
97
+ `quantjourney-common-sdk` for
98
+ auth/transport, exposes dynamic qj-api connector clients, and keeps warehouse
99
+ data-plane concerns in `quantjourney-data`.
@@ -0,0 +1,85 @@
1
+ # quantjourney-api
2
+
3
+ Python SDK for the public/product `qj-api` contract.
4
+
5
+ Canonical package metadata:
6
+
7
+ | Field | Value |
8
+ | --- | --- |
9
+ | Repository | `_repo_qj_api_sdk` |
10
+ | Distribution | `quantjourney-api` |
11
+ | Python import | `qj.api` |
12
+ | Runtime client | `QJ` |
13
+ | Backend boundary | `qj-api` / `https://api.quantjourney.cloud` |
14
+ | Version | `0.1.1` |
15
+ | Git tag | `api-v0.1.1` |
16
+
17
+ `quantjourney-api` is the public product/API SDK. The `quantjourney` umbrella
18
+ package lives in `packages/quantjourney` and installs `quantjourney-api`.
19
+
20
+ ```python
21
+ from qj.api import QJ, QuantJourneyAPI
22
+
23
+ qj = QJ.from_env()
24
+ health = qj.health()
25
+ capabilities = qj.capabilities()
26
+
27
+ # Provider connectors through qj-api
28
+ profile = qj.fmp.get_company_profile(symbol="NVDA")
29
+ gdp = qj.fred.get_series(series_id="GDP")
30
+ filings = qj.sec.get_recent_filings(limit=5)
31
+
32
+ # Provider-agnostic domain routes
33
+ routes = qj.domains.list()
34
+ prices = qj.domains.equity.pricing.get_historical_prices(symbol="NVDA")
35
+
36
+ # Product API surfaces
37
+ analytics = qj.analytics.registry()
38
+ ratios = qj.ratios.available(source="eod")
39
+ indicators = qj.technical.available()
40
+
41
+ # Compatibility aliases
42
+ qj2 = QuantJourneyAPI.from_env()
43
+ ```
44
+
45
+ Local smoke from this checkout:
46
+
47
+ ```bash
48
+ python scripts/qj_api_smoke.py --env .env
49
+ python scripts/endpoint_matrix.py --env .env
50
+ python scripts/endpoint_matrix.py --env .env --list
51
+ python scripts/endpoint_matrix.py --env .env --group connectors --allow-failures
52
+ python scripts/endpoint_matrix.py --env .env --group connector_examples --include-connector-examples --allow-failures
53
+ python scripts/connector_matrix.py --env .env
54
+ ```
55
+
56
+ `scripts/connectors.json` stores one smoke example per connector. The endpoint
57
+ matrix verifies every connector route; `connector_matrix.py` runs the examples
58
+ and prints a compact result table.
59
+
60
+ Environment variables:
61
+
62
+ | Variable | Purpose |
63
+ | --- | --- |
64
+ | `QJ_API_URL` | qj-api base URL, default `https://api.quantjourney.cloud` |
65
+ | `QJ_API_TOKEN` | bearer token |
66
+ | `QJ_AUTH_URL` | qj-auth base URL for token exchange |
67
+ | `QJ_API_KEY` | `QJ_live_*` API key used to mint `QJ_API_TOKEN` |
68
+ | `QJ_API_CLIENT_ID` | service account client id for `client_credentials` |
69
+ | `QJ_API_CLIENT_SECRET` | `QJ_svc_*` service secret for `client_credentials` |
70
+ | `QJ_API_TENANT_ID` | optional tenant projection |
71
+ | `QJ_API_TEAM_ID` | optional team projection |
72
+
73
+ Install from PyPI:
74
+
75
+ ```bash
76
+ pip install quantjourney-api==0.1.1
77
+ ```
78
+
79
+ `quantjourney-common-sdk` is installed automatically as a dependency. Local
80
+ development can still use editable path installs for sibling checkouts.
81
+
82
+ The new surface keeps the smaller `qj.api` namespace, uses
83
+ `quantjourney-common-sdk` for
84
+ auth/transport, exposes dynamic qj-api connector clients, and keeps warehouse
85
+ data-plane concerns in `quantjourney-data`.
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "quantjourney-api"
7
+ version = "0.1.1"
8
+ description = "Python SDK for QuantJourney qj-api product contracts."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "QuantJourney" }]
12
+ license = "LicenseRef-Proprietary"
13
+ dependencies = [
14
+ "quantjourney-common-sdk>=0.1.1,<0.2",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=8.0.0",
20
+ ]
21
+ frames = [
22
+ "pandas>=2.0.0",
23
+ ]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["."]
27
+ include = ["qj.*"]
28
+ namespaces = true
29
+
30
+ [tool.pytest.ini_options]
31
+ testpaths = ["_tests"]
@@ -0,0 +1,14 @@
1
+ """qj.api public/product API SDK."""
2
+
3
+ from .client import QJ, QuantJourney, QuantJourneyAPI
4
+ from .resources import CONNECTORS, ConnectorClient, DomainsClient, DomainProxy
5
+
6
+ __all__ = [
7
+ "CONNECTORS",
8
+ "ConnectorClient",
9
+ "DomainsClient",
10
+ "DomainProxy",
11
+ "QJ",
12
+ "QuantJourney",
13
+ "QuantJourneyAPI",
14
+ ]
@@ -0,0 +1,137 @@
1
+ """qj-api product SDK client surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ from qj.common import AuthConfig, HttpClient, QJResponse
10
+
11
+ from .resources import (
12
+ CONNECTORS,
13
+ AnalyticsClient,
14
+ AuthClient,
15
+ BacktesterClient,
16
+ ConnectorClient,
17
+ DomainsClient,
18
+ InsiderClient,
19
+ RatiosClient,
20
+ TechnicalClient,
21
+ UniverseClient,
22
+ )
23
+
24
+ DEFAULT_API_URL = "https://api.quantjourney.cloud"
25
+
26
+
27
+ class QJ:
28
+ """Runtime client for qj-api product contracts."""
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ base_url: str = DEFAULT_API_URL,
34
+ token: str | None = None,
35
+ api_key: str | None = None,
36
+ auth_url: str | None = None,
37
+ client_id: str | None = None,
38
+ client_secret: str | None = None,
39
+ tenant_id: str | None = None,
40
+ team_id: str | None = None,
41
+ timeout: float = 30.0,
42
+ http_client: HttpClient | None = None,
43
+ ) -> None:
44
+ auth = AuthConfig(
45
+ bearer_token=token,
46
+ api_key=api_key,
47
+ client_id=client_id,
48
+ client_secret=client_secret,
49
+ auth_url=auth_url or "https://auth.quantjourney.cloud",
50
+ tenant_id=tenant_id,
51
+ team_id=team_id,
52
+ )
53
+ self.http = http_client or HttpClient(
54
+ base_url,
55
+ auth=auth,
56
+ timeout=timeout,
57
+ user_agent="qj.api/0.1.0",
58
+ )
59
+ self._refresh_token: str | None = None
60
+ self.auth = AuthClient(self)
61
+ self.domains = DomainsClient(self)
62
+ self.analytics = AnalyticsClient(self)
63
+ self.bt = BacktesterClient(self)
64
+ self.backtester = self.bt
65
+ self.universe = UniverseClient(self)
66
+ self.ratios = RatiosClient(self)
67
+ self.technical = TechnicalClient(self)
68
+ self.insider = InsiderClient(self)
69
+ self._connectors: dict[str, ConnectorClient] = {
70
+ connector: ConnectorClient(self, connector) for connector in CONNECTORS
71
+ }
72
+ for connector, client in self._connectors.items():
73
+ setattr(self, connector, client)
74
+
75
+ @classmethod
76
+ def from_env(cls) -> "QJ":
77
+ auth = AuthConfig.from_env("QJ_API")
78
+ return cls(
79
+ base_url=os.getenv("QJ_API_URL") or os.getenv("QJ_API_BASE_URL") or DEFAULT_API_URL,
80
+ token=auth.bearer_token,
81
+ api_key=auth.api_key,
82
+ auth_url=auth.auth_url,
83
+ client_id=auth.client_id,
84
+ client_secret=auth.client_secret,
85
+ tenant_id=auth.tenant_id,
86
+ team_id=auth.team_id,
87
+ timeout=float(os.getenv("QJ_API_TIMEOUT", "30")),
88
+ )
89
+
90
+ def health(self) -> QJResponse:
91
+ return self.http.get("/health")
92
+
93
+ def contract_version(self) -> QJResponse:
94
+ return self.http.get("/api/v1/contract")
95
+
96
+ def capabilities(self) -> QJResponse:
97
+ return self.http.get("/api/v1/capabilities")
98
+
99
+ def upstreams(self) -> QJResponse:
100
+ return self.http.get("/api/v1/upstreams")
101
+
102
+ def get(self, path: str, **params: Any) -> QJResponse:
103
+ return self.http.get(path, params=_clean(params))
104
+
105
+ def post(self, path: str, json: Mapping[str, Any] | None = None) -> QJResponse:
106
+ return self.http.post(path, json=json)
107
+
108
+ def connector(self, name: str) -> ConnectorClient:
109
+ key = name.strip().lower().replace("-", "_")
110
+ if key not in self._connectors:
111
+ self._connectors[key] = ConnectorClient(self, key)
112
+ return self._connectors[key]
113
+
114
+ def domain(self, route_prefix: str):
115
+ proxy = self.domains
116
+ for part in route_prefix.split("."):
117
+ if part:
118
+ proxy = getattr(proxy, part)
119
+ return proxy
120
+
121
+ def __getattr__(self, name: str):
122
+ if name.startswith("_"):
123
+ raise AttributeError(name)
124
+ if name in CONNECTORS:
125
+ return self.connector(name)
126
+ return self.domain(name)
127
+
128
+
129
+ def _clean(params: Mapping[str, Any] | None) -> dict[str, Any] | None:
130
+ if not params:
131
+ return None
132
+ cleaned = {key: value for key, value in params.items() if value is not None}
133
+ return cleaned or None
134
+
135
+
136
+ QuantJourneyAPI = QJ
137
+ QuantJourney = QJ
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,432 @@
1
+ """Resource clients for the qj-api SDK surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any
7
+
8
+ import requests
9
+
10
+ from qj.common import QJResponse
11
+
12
+
13
+ CONNECTORS: tuple[str, ...] = (
14
+ "bea",
15
+ "bis",
16
+ "bls",
17
+ "boc",
18
+ "cboe",
19
+ "ccxt",
20
+ "census",
21
+ "cftc",
22
+ "cnnf",
23
+ "coingecko",
24
+ "dbnomics",
25
+ "defillama",
26
+ "ecb",
27
+ "eia",
28
+ "eod",
29
+ "esma",
30
+ "eurostat",
31
+ "fdic",
32
+ "ff",
33
+ "finnhub",
34
+ "finra",
35
+ "fmp",
36
+ "fred",
37
+ "gdelt",
38
+ "gleif",
39
+ "hud",
40
+ "imf",
41
+ "kalshi",
42
+ "multpl",
43
+ "nasdaqtrader",
44
+ "nyfed",
45
+ "oecd",
46
+ "ofac",
47
+ "ofr",
48
+ "openfigi",
49
+ "polymarket",
50
+ "rba",
51
+ "reddit",
52
+ "sec",
53
+ "secadv",
54
+ "secftd",
55
+ "snb",
56
+ "stocktwits",
57
+ "stooq",
58
+ "tiingo",
59
+ "treasury",
60
+ "usda",
61
+ "worldbank",
62
+ "yf",
63
+ )
64
+
65
+
66
+ class _Resource:
67
+ def __init__(self, qj: Any) -> None:
68
+ self._qj = qj
69
+
70
+ def _get(self, path: str, **params: Any) -> QJResponse:
71
+ return self._qj.http.get(path, params=_clean(params))
72
+
73
+ def _post(self, path: str, json: Mapping[str, Any] | None = None, **params: Any) -> QJResponse:
74
+ return self._qj.http.post(path, json=_clean_body(json), params=_clean(params))
75
+
76
+ def _request(
77
+ self,
78
+ method: str,
79
+ path: str,
80
+ *,
81
+ params: Mapping[str, Any] | None = None,
82
+ json: Mapping[str, Any] | None = None,
83
+ ) -> QJResponse:
84
+ if hasattr(self._qj.http, "request"):
85
+ return self._qj.http.request(method, path, params=_clean(params), json=_clean_body(json))
86
+ method = method.upper()
87
+ if method == "GET":
88
+ return self._qj.http.get(path, params=_clean(params))
89
+ if method == "POST":
90
+ return self._qj.http.post(path, json=_clean_body(json))
91
+ raise AttributeError("Configured http_client does not expose request().")
92
+
93
+
94
+ class ConnectorClient(_Resource):
95
+ """Dynamic client for qj-api provider connector routes.
96
+
97
+ Examples:
98
+ qj.fmp.get_company_profile(symbol="NVDA")
99
+ qj.fred.get_series(series_id="GDP")
100
+ qj.yf.get_historical_prices(symbol="AAPL")
101
+ """
102
+
103
+ def __init__(self, qj: Any, connector: str) -> None:
104
+ super().__init__(qj)
105
+ self.connector = connector
106
+
107
+ def call(self, method: str, **params: Any) -> QJResponse:
108
+ return self._post(f"/{self.connector}/{method}", _normalize_connector_payload(self.connector, method, params))
109
+
110
+ def __getattr__(self, method: str):
111
+ if method.startswith("_"):
112
+ raise AttributeError(method)
113
+
114
+ def _call(**params: Any) -> QJResponse:
115
+ return self.call(method, **params)
116
+
117
+ _call.__name__ = method
118
+ _call.__doc__ = f"POST /{self.connector}/{method}"
119
+ return _call
120
+
121
+ def __repr__(self) -> str:
122
+ return f"ConnectorClient({self.connector!r})"
123
+
124
+
125
+ class DomainProxy:
126
+ """Chainable proxy for qj-api `/d/<domain.route>` calls."""
127
+
128
+ def __init__(self, domains: "DomainsClient", path: str) -> None:
129
+ self._domains = domains
130
+ self._path = path
131
+
132
+ def __getattr__(self, name: str):
133
+ if name.startswith("_"):
134
+ raise AttributeError(name)
135
+ if _looks_like_callable_domain_method(name):
136
+ route = f"{self._path}.{name}"
137
+
138
+ def _call(**payload: Any) -> QJResponse:
139
+ return self._domains.call(route, **payload)
140
+
141
+ _call.__name__ = name
142
+ _call.__doc__ = f"POST /d/{route}"
143
+ return _call
144
+ return DomainProxy(self._domains, f"{self._path}.{name}")
145
+
146
+ def __repr__(self) -> str:
147
+ return f"DomainProxy({self._path!r})"
148
+
149
+
150
+ class DomainsClient(_Resource):
151
+ """Discovery and invocation client for qj-api domain routes."""
152
+
153
+ def list(self) -> QJResponse:
154
+ return self._get("/d/")
155
+
156
+ def tree(
157
+ self,
158
+ *,
159
+ scope: str = "all",
160
+ include_aliases: bool = True,
161
+ include_unavailable: bool = True,
162
+ ) -> QJResponse:
163
+ return self._get(
164
+ "/d/tree",
165
+ scope=scope,
166
+ include_aliases=_bool(include_aliases),
167
+ include_unavailable=_bool(include_unavailable),
168
+ )
169
+
170
+ def aliases(self) -> QJResponse:
171
+ return self._get("/d/aliases")
172
+
173
+ def synonyms(self) -> QJResponse:
174
+ return self._get("/d/synonyms")
175
+
176
+ def describe(self, route: str) -> QJResponse:
177
+ return self._get(f"/d/describe/{route}")
178
+
179
+ def call(self, route: str, **payload: Any) -> QJResponse:
180
+ return self._post(f"/d/{route}", payload)
181
+
182
+ def __getattr__(self, name: str):
183
+ if name.startswith("_"):
184
+ raise AttributeError(name)
185
+ return DomainProxy(self, name)
186
+
187
+
188
+ class AuthClient(_Resource):
189
+ """qj-auth helpers that wire successful login into the SDK client."""
190
+
191
+ def login(self, email: str, password: str, *, tenant_id: str | None = None) -> Mapping[str, Any]:
192
+ payload: dict[str, Any] = {"email": email, "password": password}
193
+ if tenant_id:
194
+ payload["tenant_id"] = tenant_id
195
+ body = self._auth_request("POST", "/auth/login", json=payload)
196
+ token = body.get("access_token") if isinstance(body, dict) else None
197
+ if token:
198
+ self._qj.http.auth.bearer_token = str(token)
199
+ self._qj.http.auth.invalidate()
200
+ refresh = body.get("refresh_token") if isinstance(body, dict) else None
201
+ if refresh:
202
+ self._qj._refresh_token = str(refresh)
203
+ return body
204
+
205
+ def refresh(self) -> Mapping[str, Any]:
206
+ refresh_token = getattr(self._qj, "_refresh_token", None)
207
+ if not refresh_token:
208
+ raise RuntimeError("No refresh token available. Call auth.login() first.")
209
+ body = self._auth_request("POST", "/auth/refresh", json={"refresh_token": refresh_token})
210
+ token = body.get("access_token") if isinstance(body, dict) else None
211
+ if token:
212
+ self._qj.http.auth.bearer_token = str(token)
213
+ self._qj.http.auth.invalidate()
214
+ refresh = body.get("refresh_token") if isinstance(body, dict) else None
215
+ if refresh:
216
+ self._qj._refresh_token = str(refresh)
217
+ return body
218
+
219
+ def whoami(self) -> Mapping[str, Any]:
220
+ return self._auth_request("GET", "/auth/whoami")
221
+
222
+ def _auth_request(self, method: str, path: str, *, json: Mapping[str, Any] | None = None) -> Mapping[str, Any]:
223
+ headers: dict[str, str] = {"Accept": "application/json"}
224
+ token = self._qj.http.auth.token()
225
+ if token:
226
+ headers["Authorization"] = f"Bearer {token}"
227
+ url = f"{self._qj.http.auth.auth_url.rstrip('/')}/{path.lstrip('/')}"
228
+ response = requests.request(method, url, json=json, headers=headers, timeout=self._qj.http.timeout)
229
+ body = response.json() if response.text else {}
230
+ response.raise_for_status()
231
+ return body
232
+
233
+
234
+ class AnalyticsClient(_Resource):
235
+ def info(self) -> QJResponse:
236
+ return self._get("/analytics/")
237
+
238
+ def registry(self) -> QJResponse:
239
+ return self._get("/analytics/registry")
240
+
241
+ def hv(self, **payload: Any) -> QJResponse:
242
+ return self._post("/analytics/hv", payload)
243
+
244
+ def hv_plot(self, **payload: Any) -> QJResponse:
245
+ return self._post("/analytics/hv_plot", payload)
246
+
247
+ def iv(self, **payload: Any) -> QJResponse:
248
+ return self._post("/analytics/iv", payload)
249
+
250
+ def iv_plot(self, **payload: Any) -> QJResponse:
251
+ return self._post("/analytics/iv_plot", payload)
252
+
253
+ def greeks(self, **payload: Any) -> QJResponse:
254
+ return self._post("/analytics/greeks", payload)
255
+
256
+ def greeks_plot(self, **payload: Any) -> QJResponse:
257
+ return self._post("/analytics/greeks_plot", payload)
258
+
259
+ def rolling_plot(self, **payload: Any) -> QJResponse:
260
+ return self._post("/analytics/rolling_plot", payload)
261
+
262
+ def beta_plot(self, **payload: Any) -> QJResponse:
263
+ return self._post("/analytics/beta_plot", payload)
264
+
265
+ def iv_surface_plot(self, **payload: Any) -> QJResponse:
266
+ return self._post("/analytics/iv_surface_plot", payload)
267
+
268
+ def factors_beta(self, **payload: Any) -> QJResponse:
269
+ return self._post("/analytics/factors_beta", payload)
270
+
271
+ def factors_beta_plot(self, **payload: Any) -> QJResponse:
272
+ return self._post("/analytics/factors_beta_plot", payload)
273
+
274
+ def job(self, job_id: str) -> QJResponse:
275
+ return self._get(f"/analytics/jobs/{job_id}")
276
+
277
+ def create_job(self, **payload: Any) -> QJResponse:
278
+ return self._post("/analytics/jobs", payload)
279
+
280
+
281
+ class BacktesterClient(_Resource):
282
+ def info(self) -> QJResponse:
283
+ return self._get("/bt/")
284
+
285
+ def prepare(self, **payload: Any) -> QJResponse:
286
+ return self._post("/bt/prepare", payload)
287
+
288
+ def plots(self, **payload: Any) -> QJResponse:
289
+ return self._post("/bt/plots", payload)
290
+
291
+ def sessions(self) -> QJResponse:
292
+ return self._get("/bt/sessions")
293
+
294
+ def session(self, session_id: str) -> QJResponse:
295
+ return self._get(f"/bt/sessions/{session_id}")
296
+
297
+ def datasets(self) -> QJResponse:
298
+ return self._get("/bt/datasets")
299
+
300
+ def dataset(self, dataset_id: str) -> QJResponse:
301
+ return self._get(f"/bt/datasets/{dataset_id}")
302
+
303
+ def calc_registry(self) -> QJResponse:
304
+ return self._get("/bt/calc/registry")
305
+
306
+ def calc_portfolio(self, **payload: Any) -> QJResponse:
307
+ return self._post("/bt/calc/portfolio", payload)
308
+
309
+ def calc_instrument(self, **payload: Any) -> QJResponse:
310
+ return self._post("/bt/calc/instrument", payload)
311
+
312
+
313
+ class UniverseClient(_Resource):
314
+ def info(self) -> QJResponse:
315
+ return self._get("/universe/")
316
+
317
+ def preview(self, spec: Mapping[str, Any] | None = None, *, offset: int = 0, limit: int = 100) -> QJResponse:
318
+ return self._post("/universe/preview", spec or {}, offset=offset, limit=limit)
319
+
320
+ def build(self, spec: Mapping[str, Any] | None = None) -> QJResponse:
321
+ return self._post("/universe/build", spec or {})
322
+
323
+ def list(self, *, offset: int = 0, limit: int = 100) -> QJResponse:
324
+ return self._get("/universe/list", offset=offset, limit=limit)
325
+
326
+ def get(self, universe_id: str) -> QJResponse:
327
+ return self._get(f"/universe/{universe_id}")
328
+
329
+
330
+ class RatiosClient(_Resource):
331
+ def available(self, *, source: str = "eod") -> QJResponse:
332
+ return self._get("/ratios/available", source=source)
333
+
334
+ def series(
335
+ self,
336
+ ratio: str,
337
+ ticker: str,
338
+ *,
339
+ exchange: str = "US",
340
+ source: str = "eod",
341
+ start: str | None = None,
342
+ end: str | None = None,
343
+ compute: bool = False,
344
+ period: str = "q",
345
+ years: int | None = None,
346
+ pub_lag_days: int | None = None,
347
+ calendar_code: str | None = None,
348
+ ) -> QJResponse:
349
+ return self._get(
350
+ "/ratios/series",
351
+ ratio=ratio,
352
+ ticker=ticker,
353
+ exchange=exchange,
354
+ source=source,
355
+ start=start,
356
+ end=end,
357
+ compute=compute,
358
+ period=period,
359
+ years=years,
360
+ pub_lag_days=pub_lag_days,
361
+ calendar_code=calendar_code,
362
+ )
363
+
364
+
365
+ class TechnicalClient(_Resource):
366
+ def available(self) -> QJResponse:
367
+ return self._get("/technical/available")
368
+
369
+ def series(self, indicator: str, ticker: str, **payload: Any) -> QJResponse:
370
+ body = {"indicator": indicator, "ticker": ticker, **payload}
371
+ return self._post("/technical/series", body)
372
+
373
+
374
+ class InsiderClient(_Resource):
375
+ def info(self) -> QJResponse:
376
+ return self._get("/insider/")
377
+
378
+ def scan(self, symbols: Sequence[str], **payload: Any) -> QJResponse:
379
+ return self._post("/insider/scan", {"symbols": list(symbols), **payload})
380
+
381
+ def new(self, symbols: Sequence[str] | None = None, **payload: Any) -> QJResponse:
382
+ body = {"symbols": list(symbols) if symbols is not None else [], **payload}
383
+ return self._post("/insider/new", body)
384
+
385
+ def summary(self, symbols: Sequence[str], **payload: Any) -> QJResponse:
386
+ return self._post("/insider/summary", {"symbols": list(symbols), **payload})
387
+
388
+ def latest_trades(self, symbols: Sequence[str], **payload: Any) -> QJResponse:
389
+ return self._post("/insider/latest_trades", {"symbols": list(symbols), **payload})
390
+
391
+ def summary_universe(self, **payload: Any) -> QJResponse:
392
+ return self._post("/insider/summary_universe", payload)
393
+
394
+
395
+ def _normalize_connector_payload(connector: str, method: str, params: Mapping[str, Any]) -> dict[str, Any] | None:
396
+ payload = dict(params)
397
+ if connector in {"eod", "fmp", "yf"} and method in {
398
+ "get_historical_prices",
399
+ "get_intraday_prices",
400
+ "get_real_time_prices",
401
+ "get_forex_intraday_prices",
402
+ }:
403
+ if "symbols" not in payload and "symbol" in payload:
404
+ symbol = payload.pop("symbol")
405
+ if isinstance(symbol, str) and "," in symbol:
406
+ payload["symbols"] = [item.strip() for item in symbol.split(",") if item.strip()]
407
+ else:
408
+ payload["symbols"] = [symbol] if isinstance(symbol, str) else symbol
409
+ # FastAPI connector methods expect a JSON object body even when the
410
+ # parameter model has no required fields.
411
+ return {key: value for key, value in payload.items() if value is not None}
412
+
413
+
414
+ def _looks_like_callable_domain_method(name: str) -> bool:
415
+ return name.startswith(("get_", "list_", "search_", "fetch_", "query_", "calculate_", "compute_"))
416
+
417
+
418
+ def _bool(value: bool) -> str:
419
+ return "true" if value else "false"
420
+
421
+
422
+ def _clean(params: Mapping[str, Any] | None) -> dict[str, Any] | None:
423
+ if not params:
424
+ return None
425
+ cleaned = {key: value for key, value in params.items() if value is not None}
426
+ return cleaned or None
427
+
428
+
429
+ def _clean_body(params: Mapping[str, Any] | None) -> dict[str, Any] | None:
430
+ if params is None:
431
+ return None
432
+ return {key: value for key, value in params.items() if value is not None}
@@ -0,0 +1,14 @@
1
+ """Compatibility alias for the canonical :mod:`qj.api` package."""
2
+
3
+ from qj.api import QJ, QuantJourney, QuantJourneyAPI
4
+ from qj.api.resources import CONNECTORS, ConnectorClient, DomainsClient, DomainProxy
5
+
6
+ __all__ = [
7
+ "CONNECTORS",
8
+ "ConnectorClient",
9
+ "DomainsClient",
10
+ "DomainProxy",
11
+ "QJ",
12
+ "QuantJourney",
13
+ "QuantJourneyAPI",
14
+ ]
@@ -0,0 +1,3 @@
1
+ """Compatibility alias for :mod:`qj.api.client`."""
2
+
3
+ from qj.api.client import * # noqa: F401,F403
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,3 @@
1
+ """Compatibility alias for :mod:`qj.api.resources`."""
2
+
3
+ from qj.api.resources import * # noqa: F401,F403
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: quantjourney-api
3
+ Version: 0.1.1
4
+ Summary: Python SDK for QuantJourney qj-api product contracts.
5
+ Author: QuantJourney
6
+ License-Expression: LicenseRef-Proprietary
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: quantjourney-common-sdk<0.2,>=0.1.1
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
12
+ Provides-Extra: frames
13
+ Requires-Dist: pandas>=2.0.0; extra == "frames"
14
+
15
+ # quantjourney-api
16
+
17
+ Python SDK for the public/product `qj-api` contract.
18
+
19
+ Canonical package metadata:
20
+
21
+ | Field | Value |
22
+ | --- | --- |
23
+ | Repository | `_repo_qj_api_sdk` |
24
+ | Distribution | `quantjourney-api` |
25
+ | Python import | `qj.api` |
26
+ | Runtime client | `QJ` |
27
+ | Backend boundary | `qj-api` / `https://api.quantjourney.cloud` |
28
+ | Version | `0.1.1` |
29
+ | Git tag | `api-v0.1.1` |
30
+
31
+ `quantjourney-api` is the public product/API SDK. The `quantjourney` umbrella
32
+ package lives in `packages/quantjourney` and installs `quantjourney-api`.
33
+
34
+ ```python
35
+ from qj.api import QJ, QuantJourneyAPI
36
+
37
+ qj = QJ.from_env()
38
+ health = qj.health()
39
+ capabilities = qj.capabilities()
40
+
41
+ # Provider connectors through qj-api
42
+ profile = qj.fmp.get_company_profile(symbol="NVDA")
43
+ gdp = qj.fred.get_series(series_id="GDP")
44
+ filings = qj.sec.get_recent_filings(limit=5)
45
+
46
+ # Provider-agnostic domain routes
47
+ routes = qj.domains.list()
48
+ prices = qj.domains.equity.pricing.get_historical_prices(symbol="NVDA")
49
+
50
+ # Product API surfaces
51
+ analytics = qj.analytics.registry()
52
+ ratios = qj.ratios.available(source="eod")
53
+ indicators = qj.technical.available()
54
+
55
+ # Compatibility aliases
56
+ qj2 = QuantJourneyAPI.from_env()
57
+ ```
58
+
59
+ Local smoke from this checkout:
60
+
61
+ ```bash
62
+ python scripts/qj_api_smoke.py --env .env
63
+ python scripts/endpoint_matrix.py --env .env
64
+ python scripts/endpoint_matrix.py --env .env --list
65
+ python scripts/endpoint_matrix.py --env .env --group connectors --allow-failures
66
+ python scripts/endpoint_matrix.py --env .env --group connector_examples --include-connector-examples --allow-failures
67
+ python scripts/connector_matrix.py --env .env
68
+ ```
69
+
70
+ `scripts/connectors.json` stores one smoke example per connector. The endpoint
71
+ matrix verifies every connector route; `connector_matrix.py` runs the examples
72
+ and prints a compact result table.
73
+
74
+ Environment variables:
75
+
76
+ | Variable | Purpose |
77
+ | --- | --- |
78
+ | `QJ_API_URL` | qj-api base URL, default `https://api.quantjourney.cloud` |
79
+ | `QJ_API_TOKEN` | bearer token |
80
+ | `QJ_AUTH_URL` | qj-auth base URL for token exchange |
81
+ | `QJ_API_KEY` | `QJ_live_*` API key used to mint `QJ_API_TOKEN` |
82
+ | `QJ_API_CLIENT_ID` | service account client id for `client_credentials` |
83
+ | `QJ_API_CLIENT_SECRET` | `QJ_svc_*` service secret for `client_credentials` |
84
+ | `QJ_API_TENANT_ID` | optional tenant projection |
85
+ | `QJ_API_TEAM_ID` | optional team projection |
86
+
87
+ Install from PyPI:
88
+
89
+ ```bash
90
+ pip install quantjourney-api==0.1.1
91
+ ```
92
+
93
+ `quantjourney-common-sdk` is installed automatically as a dependency. Local
94
+ development can still use editable path installs for sibling checkouts.
95
+
96
+ The new surface keeps the smaller `qj.api` namespace, uses
97
+ `quantjourney-common-sdk` for
98
+ auth/transport, exposes dynamic qj-api connector clients, and keeps warehouse
99
+ data-plane concerns in `quantjourney-data`.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ qj/api/__init__.py
4
+ qj/api/client.py
5
+ qj/api/py.typed
6
+ qj/api/resources.py
7
+ qj/sdk/__init__.py
8
+ qj/sdk/client.py
9
+ qj/sdk/py.typed
10
+ qj/sdk/resources.py
11
+ quantjourney_api.egg-info/PKG-INFO
12
+ quantjourney_api.egg-info/SOURCES.txt
13
+ quantjourney_api.egg-info/dependency_links.txt
14
+ quantjourney_api.egg-info/requires.txt
15
+ quantjourney_api.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ quantjourney-common-sdk<0.2,>=0.1.1
2
+
3
+ [dev]
4
+ pytest>=8.0.0
5
+
6
+ [frames]
7
+ pandas>=2.0.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+