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.
- quantjourney_api-0.1.1/PKG-INFO +99 -0
- quantjourney_api-0.1.1/README.md +85 -0
- quantjourney_api-0.1.1/pyproject.toml +31 -0
- quantjourney_api-0.1.1/qj/api/__init__.py +14 -0
- quantjourney_api-0.1.1/qj/api/client.py +137 -0
- quantjourney_api-0.1.1/qj/api/py.typed +1 -0
- quantjourney_api-0.1.1/qj/api/resources.py +432 -0
- quantjourney_api-0.1.1/qj/sdk/__init__.py +14 -0
- quantjourney_api-0.1.1/qj/sdk/client.py +3 -0
- quantjourney_api-0.1.1/qj/sdk/py.typed +1 -0
- quantjourney_api-0.1.1/qj/sdk/resources.py +3 -0
- quantjourney_api-0.1.1/quantjourney_api.egg-info/PKG-INFO +99 -0
- quantjourney_api-0.1.1/quantjourney_api.egg-info/SOURCES.txt +15 -0
- quantjourney_api-0.1.1/quantjourney_api.egg-info/dependency_links.txt +1 -0
- quantjourney_api-0.1.1/quantjourney_api.egg-info/requires.txt +7 -0
- quantjourney_api-0.1.1/quantjourney_api.egg-info/top_level.txt +1 -0
- quantjourney_api-0.1.1/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qj
|