Skip to content

Commit 29ddeef

Browse files
authored
chore(release): 8.2.0 (#185)
## Added - New v2 export methods in `ExportApi`: `export_signals_v2`, `run_export_signals_v2`, and `export_signals_v2_bytes`. - New CLI script `exabel.scripts.export_signals_v2` that runs a v2 signal export and writes the raw server response to disk. Output format is selected by the `--filename` extension (`.csv`, `.xlsx`, `.json`, `.feather`, `.parquet`). - New optional dependency group `export`. Install with `pip install 'exabel[export]'`. ## Changed - `pyarrow` is required for `export_signals_v2` and `run_export_signals_v2`. ## Deprecated - `signal_query_v2` and `run_signal_query_v2` are deprecated; use `export_signals_v2` and `run_export_signals_v2` instead. - `run_query_bytes` with `file_format="pickle"` is deprecated. ## Removed - The v2 export endpoint no longer accepts `output_format="pickle"`.
1 parent ad00d4b commit 29ddeef

9 files changed

Lines changed: 1100 additions & 111 deletions

File tree

exabel/client/api/export_api.py

Lines changed: 221 additions & 60 deletions
Large diffs are not rendered by default.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import argparse
2+
import sys
3+
from datetime import timedelta
4+
from time import time
5+
from typing import Sequence
6+
7+
import pandas as pd
8+
9+
from exabel import ExabelClient
10+
from exabel.client.api.data_classes.derived_signal import DerivedSignal
11+
from exabel.scripts.base_script import BaseScript
12+
13+
_EXTENSION_TO_FILE_FORMAT = {
14+
"csv": "csv",
15+
"xlsx": "excel",
16+
"json": "json",
17+
"feather": "feather",
18+
"parquet": "parquet",
19+
}
20+
21+
22+
def _validate_filename(filename: str) -> str:
23+
"""Ensure the output file has one of the server-supported extensions."""
24+
extension = filename.rsplit(".", 1)[-1].lower()
25+
if extension not in _EXTENSION_TO_FILE_FORMAT:
26+
raise argparse.ArgumentTypeError(
27+
f"Unknown file extension {extension!r}. Supported: "
28+
+ ", ".join(f".{e}" for e in _EXTENSION_TO_FILE_FORMAT)
29+
)
30+
return filename
31+
32+
33+
def _parse_label_expression(value: str) -> DerivedSignal:
34+
"""Parse a ``LABEL=EXPRESSION`` CLI value into a DerivedSignal.
35+
36+
The label must be non-empty; the expression is everything after the first
37+
``=`` so expressions containing ``=`` themselves (e.g. comparisons) still
38+
parse cleanly.
39+
"""
40+
if "=" not in value:
41+
raise argparse.ArgumentTypeError(
42+
f"--expression value must be formatted as LABEL=EXPRESSION, got: {value!r}"
43+
)
44+
label, expression = value.split("=", 1)
45+
label = label.strip()
46+
expression = expression.strip()
47+
if not label or not expression:
48+
raise argparse.ArgumentTypeError(
49+
f"--expression value must have non-empty LABEL and EXPRESSION, got: {value!r}"
50+
)
51+
return DerivedSignal(name=None, label=label, expression=expression)
52+
53+
54+
class ExportSignalsV2(BaseScript):
55+
"""Export time series using the v2 signal export endpoint and write the raw
56+
server response to a file.
57+
58+
Unlike ``export_signals``, this supports multi-time-series signals (e.g.
59+
signals whose expression uses ``for_type(...)`` and returns one series per
60+
sub-entity). Entities must be specified by resource name or tag;
61+
bloomberg_ticker / factset_id are not supported by the v2 endpoint.
62+
63+
The output file contains the server's wire response verbatim (no pandas
64+
round-trip, no timezone normalization). Parquet and feather preserve the
65+
full multi-level column headers; csv, excel, and json are flattened per the
66+
server's serialization of those formats. Running this script does not
67+
require ``pyarrow`` — only the SDK and a network connection.
68+
"""
69+
70+
def __init__(self, argv: Sequence[str], description: str):
71+
super().__init__(argv, description)
72+
self.parser.add_argument(
73+
"--signal",
74+
nargs="+",
75+
required=False,
76+
default=[],
77+
type=str,
78+
help=(
79+
"Signal label(s) to export from the library. "
80+
"At least one of --signal or --expression must be given."
81+
),
82+
)
83+
self.parser.add_argument(
84+
"--expression",
85+
nargs="+",
86+
required=False,
87+
default=[],
88+
type=_parse_label_expression,
89+
help=(
90+
"DSL expression(s) to evaluate, each as LABEL=EXPRESSION. "
91+
"The LABEL becomes the column name in the output. Example: "
92+
"--expression "
93+
'\'brand_sales=data("sales").for_type("brand")\''
94+
),
95+
)
96+
entity_group = self.parser.add_mutually_exclusive_group(required=True)
97+
entity_group.add_argument(
98+
"--resource-name",
99+
nargs="+",
100+
type=str,
101+
help="Entity resource name(s) to evaluate the signal(s) for.",
102+
)
103+
entity_group.add_argument(
104+
"--tag",
105+
nargs="+",
106+
type=str,
107+
help="Tag(s) whose entities should be evaluated.",
108+
)
109+
self.parser.add_argument(
110+
"--filename",
111+
required=True,
112+
type=_validate_filename,
113+
help=(
114+
"The filename to write the raw server response to. Supported extensions: "
115+
+ ", ".join(f".{e}" for e in _EXTENSION_TO_FILE_FORMAT)
116+
),
117+
)
118+
self.parser.add_argument(
119+
"--start-date",
120+
required=False,
121+
type=pd.Timestamp,
122+
help="The first date to evaluate the signals for.",
123+
)
124+
self.parser.add_argument(
125+
"--end-date",
126+
required=False,
127+
type=pd.Timestamp,
128+
help="The last date to evaluate the signals for.",
129+
)
130+
self.parser.add_argument(
131+
"--known-time",
132+
required=False,
133+
type=pd.Timestamp,
134+
help="The point-in-time at which to retrieve the time series.",
135+
)
136+
137+
def run_script(self, client: ExabelClient, args: argparse.Namespace) -> None:
138+
start_time = time()
139+
signals: list[str | DerivedSignal] = [*args.signal, *args.expression]
140+
if not signals:
141+
self.parser.error("at least one of --signal or --expression must be provided")
142+
signal_labels = [s if isinstance(s, str) else s.label for s in signals]
143+
print("Downloading signal(s):", ", ".join(signal_labels))
144+
145+
extension = args.filename.rsplit(".", 1)[-1].lower()
146+
file_format = _EXTENSION_TO_FILE_FORMAT[extension]
147+
148+
content = client.export_api.export_signals_v2_bytes(
149+
signals,
150+
file_format=file_format,
151+
resource_name=args.resource_name,
152+
tag=args.tag,
153+
start_time=args.start_date,
154+
end_time=args.end_date,
155+
version=args.known_time,
156+
)
157+
with open(args.filename, "wb") as f:
158+
f.write(content)
159+
160+
spent_time = timedelta(seconds=int(time() - start_time))
161+
print(f"{len(content)} bytes written to {args.filename}, spent {spent_time}")
162+
163+
164+
if __name__ == "__main__":
165+
ExportSignalsV2(
166+
sys.argv,
167+
"Export time series from the Exabel API for specified signals (v2 endpoint)",
168+
).run()

0 commit comments

Comments
 (0)