Skip to content

Commit f4f8c3f

Browse files
committed
FIX: Fix handling of None in symbol list parsing
1 parent f61adb2 commit f4f8c3f

File tree

7 files changed

+100
-44
lines changed

7 files changed

+100
-44
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.39.1 - TBD
4+
5+
#### Bug fixes
6+
- Fixed an issue where a symbol list which contained a `None` would produce a convoluted exception
7+
38
## 0.39.0 - 2024-07-30
49

510
#### Enhancements

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ The library is fully compatible with the latest distribution of Anaconda 3.8 and
3232
The minimum dependencies as found in the `pyproject.toml` are also listed below:
3333
- python = "^3.8"
3434
- aiohttp = "^3.8.3"
35-
- databento-dbn = "0.19.1"
35+
- databento-dbn = "0.20.0"
3636
- numpy= ">=1.23.5"
3737
- pandas = ">=1.5.3"
3838
- pip-system-certs = ">=4.0" (Windows only)

databento/common/parsing.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -58,60 +58,70 @@ def optional_values_list_to_string(
5858
return values_list_to_string(values)
5959

6060

61-
@singledispatch
6261
def optional_symbols_list_to_list(
6362
symbols: Iterable[str | int | Integral] | str | int | Integral | None,
6463
stype_in: SType,
6564
) -> list[str]:
6665
"""
67-
Create a list from a symbols string or iterable of symbol strings (if not
68-
None).
66+
Create a list from an optional symbols string or iterable of symbol
67+
strings. If symbols is `None`, this function returns `[ALL_SYMBOLS]`.
6968
7069
Parameters
7170
----------
7271
symbols : Iterable of str or int or Number, or str or int or Number, optional
73-
The symbols to concatenate.
72+
The symbols to concatenate; or `None`.
7473
stype_in : SType
7574
The input symbology type for the request.
7675
7776
Returns
7877
-------
7978
list[str]
8079
81-
Notes
82-
-----
83-
If None is given, [ALL_SYMBOLS] is returned.
80+
See Also
81+
--------
82+
symbols_list_to_list
8483
8584
"""
86-
raise TypeError(
87-
f"`{symbols}` is not a valid type for symbol input; "
88-
"allowed types are Iterable[str | int], str, int, and None.",
89-
)
85+
if symbols is None:
86+
return [ALL_SYMBOLS]
87+
return symbols_list_to_list(symbols, stype_in)
9088

9189

92-
@optional_symbols_list_to_list.register(cls=type(None))
93-
def _(_: None, __: SType) -> list[str]:
90+
@singledispatch
91+
def symbols_list_to_list(
92+
symbols: Iterable[str | int | Integral] | str | int | Integral,
93+
stype_in: SType,
94+
) -> list[str]:
9495
"""
95-
Dispatch method for optional_symbols_list_to_list. Handles None which
96-
defaults to [ALL_SYMBOLS].
96+
Create a list from a symbols string or iterable of symbol strings.
9797
98-
See Also
99-
--------
100-
optional_symbols_list_to_list
98+
Parameters
99+
----------
100+
symbols : Iterable of str or int or Number, or str or int or Number
101+
The symbols to concatenate.
102+
stype_in : SType
103+
The input symbology type for the request.
104+
105+
Returns
106+
-------
107+
list[str]
101108
102109
"""
103-
return [ALL_SYMBOLS]
110+
raise TypeError(
111+
f"`{symbols}` is not a valid type for symbol input; "
112+
"allowed types are Iterable[str | int], str, and int.",
113+
)
104114

105115

106-
@optional_symbols_list_to_list.register(cls=Integral)
116+
@symbols_list_to_list.register(cls=Integral)
107117
def _(symbols: Integral, stype_in: SType) -> list[str]:
108118
"""
109119
Dispatch method for optional_symbols_list_to_list. Handles integral types,
110120
alerting when an integer is given for STypes that expect strings.
111121
112122
See Also
113123
--------
114-
optional_symbols_list_to_list
124+
symbols_list_to_list
115125
116126
"""
117127
if stype_in == SType.INSTRUMENT_ID:
@@ -122,15 +132,15 @@ def _(symbols: Integral, stype_in: SType) -> list[str]:
122132
)
123133

124134

125-
@optional_symbols_list_to_list.register(cls=str)
135+
@symbols_list_to_list.register(cls=str)
126136
def _(symbols: str, stype_in: SType) -> list[str]:
127137
"""
128138
Dispatch method for optional_symbols_list_to_list. Handles str, splitting
129139
on commas and validating smart symbology.
130140
131141
See Also
132142
--------
133-
optional_symbols_list_to_list
143+
symbols_list_to_list
134144
135145
"""
136146
if not symbols:
@@ -147,19 +157,19 @@ def _(symbols: str, stype_in: SType) -> list[str]:
147157
return list(map(str.upper, map(str.strip, symbol_list)))
148158

149159

150-
@optional_symbols_list_to_list.register(cls=Iterable)
160+
@symbols_list_to_list.register(cls=Iterable)
151161
def _(symbols: Iterable[Any], stype_in: SType) -> list[str]:
152162
"""
153163
Dispatch method for optional_symbols_list_to_list. Handles Iterables by
154164
dispatching the individual members.
155165
156166
See Also
157167
--------
158-
optional_symbols_list_to_list
168+
symbols_list_to_list
159169
160170
"""
161171
symbol_to_list = partial(
162-
optional_symbols_list_to_list,
172+
symbols_list_to_list,
163173
stype_in=stype_in,
164174
)
165175
aggregated: list[str] = []

databento/historical/api/batch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
from databento.common.http import check_http_error
3737
from databento.common.parsing import datetime_to_string
3838
from databento.common.parsing import optional_datetime_to_string
39-
from databento.common.parsing import optional_symbols_list_to_list
4039
from databento.common.parsing import optional_values_list_to_string
40+
from databento.common.parsing import symbols_list_to_list
4141
from databento.common.publishers import Dataset
4242
from databento.common.types import Default
4343
from databento.common.validation import validate_enum
@@ -147,7 +147,7 @@ def submit_job(
147147
148148
"""
149149
stype_in_valid = validate_enum(stype_in, SType, "stype_in")
150-
symbols_list = optional_symbols_list_to_list(symbols, stype_in_valid)
150+
symbols_list = symbols_list_to_list(symbols, stype_in_valid)
151151
data: dict[str, object | None] = {
152152
"dataset": validate_semantic_string(dataset, "dataset"),
153153
"start": datetime_to_string(start),

databento/live/protocol.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from databento.common.error import BentoError
1818
from databento.common.iterator import chunk
1919
from databento.common.parsing import optional_datetime_to_unix_nanoseconds
20-
from databento.common.parsing import optional_symbols_list_to_list
20+
from databento.common.parsing import symbols_list_to_list
2121
from databento.common.publishers import Dataset
2222
from databento.common.types import DBNRecord
2323
from databento.common.validation import validate_enum
@@ -310,7 +310,7 @@ def subscribe(
310310
)
311311

312312
stype_in_valid = validate_enum(stype_in, SType, "stype_in")
313-
symbols_list = optional_symbols_list_to_list(symbols, stype_in_valid)
313+
symbols_list = symbols_list_to_list(symbols, stype_in_valid)
314314

315315
subscriptions: list[SubscriptionRequest] = []
316316
for batch in chunk(symbols_list, SYMBOL_LIST_BATCH_SIZE):

tests/test_common_parsing.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
import numpy as np
77
import pandas as pd
88
import pytest
9+
from databento.common.constants import ALL_SYMBOLS
910
from databento.common.parsing import optional_date_to_string
1011
from databento.common.parsing import optional_datetime_to_string
1112
from databento.common.parsing import optional_datetime_to_unix_nanoseconds
1213
from databento.common.parsing import optional_symbols_list_to_list
1314
from databento.common.parsing import optional_values_list_to_string
15+
from databento.common.parsing import symbols_list_to_list
1416
from databento_dbn import SType
1517

1618

@@ -50,7 +52,9 @@ def test_maybe_values_list_to_string_given_valid_inputs_returns_expected(
5052
@pytest.mark.parametrize(
5153
"stype, symbols, expected",
5254
[
53-
pytest.param(SType.RAW_SYMBOL, None, ["ALL_SYMBOLS"]),
55+
pytest.param(SType.RAW_SYMBOL, None, TypeError),
56+
pytest.param(SType.PARENT, ["ES", None], TypeError),
57+
pytest.param(SType.PARENT, ["ES", [None]], TypeError),
5458
pytest.param(SType.PARENT, "ES.fut", ["ES.FUT"]),
5559
pytest.param(SType.PARENT, "ES,CL", ["ES", "CL"]),
5660
pytest.param(SType.PARENT, "ES,CL,", ["ES", "CL"]),
@@ -67,22 +71,24 @@ def test_maybe_values_list_to_string_given_valid_inputs_returns_expected(
6771
pytest.param(SType.PARENT, 123458, ValueError),
6872
],
6973
)
70-
def test_optional_symbols_list_to_list_given_valid_inputs_returns_expected(
74+
def test_symbols_list_to_list_given_valid_inputs_returns_expected(
7175
stype: SType,
7276
symbols: list[str] | None,
7377
expected: list[object] | type[Exception],
7478
) -> None:
7579
# Arrange, Act, Assert
7680
if isinstance(expected, list):
77-
assert optional_symbols_list_to_list(symbols, stype) == expected
81+
assert symbols_list_to_list(symbols, stype) == expected
7882
else:
7983
with pytest.raises(expected):
80-
optional_symbols_list_to_list(symbols, stype)
84+
symbols_list_to_list(symbols, stype)
8185

8286

8387
@pytest.mark.parametrize(
8488
"symbols, stype, expected",
8589
[
90+
pytest.param([12345, None], SType.INSTRUMENT_ID, TypeError),
91+
pytest.param([12345, [None]], SType.INSTRUMENT_ID, TypeError),
8692
pytest.param(12345, SType.INSTRUMENT_ID, ["12345"]),
8793
pytest.param("67890", SType.INSTRUMENT_ID, ["67890"]),
8894
pytest.param([12345, " 67890"], SType.INSTRUMENT_ID, ["12345", "67890"]),
@@ -104,7 +110,7 @@ def test_optional_symbols_list_to_list_given_valid_inputs_returns_expected(
104110
pytest.param(12345, SType.CONTINUOUS, ValueError),
105111
],
106112
)
107-
def test_optional_symbols_list_to_list_int(
113+
def test_symbols_list_to_list_int(
108114
symbols: list[int] | int | None,
109115
stype: SType,
110116
expected: list[object] | type[Exception],
@@ -117,15 +123,18 @@ def test_optional_symbols_list_to_list_int(
117123
"""
118124
# Arrange, Act, Assert
119125
if isinstance(expected, list):
120-
assert optional_symbols_list_to_list(symbols, stype) == expected
126+
assert symbols_list_to_list(symbols, stype) == expected
121127
else:
122128
with pytest.raises(expected):
123-
optional_symbols_list_to_list(symbols, stype)
129+
symbols_list_to_list(symbols, stype)
124130

125131

126132
@pytest.mark.parametrize(
127133
"symbols, stype, expected",
128134
[
135+
pytest.param(None, SType.INSTRUMENT_ID, TypeError),
136+
pytest.param([np.byte(120), None], SType.INSTRUMENT_ID, TypeError),
137+
pytest.param([np.byte(120), [None]], SType.INSTRUMENT_ID, TypeError),
129138
pytest.param(np.byte(120), SType.INSTRUMENT_ID, ["120"]),
130139
pytest.param(np.short(32_000), SType.INSTRUMENT_ID, ["32000"]),
131140
pytest.param(
@@ -140,7 +149,7 @@ def test_optional_symbols_list_to_list_int(
140149
),
141150
],
142151
)
143-
def test_optional_symbols_list_to_list_numpy(
152+
def test_symbols_list_to_list_numpy(
144153
symbols: list[int] | int | None,
145154
stype: SType,
146155
expected: list[object] | type[Exception],
@@ -153,15 +162,18 @@ def test_optional_symbols_list_to_list_numpy(
153162
"""
154163
# Arrange, Act, Assert
155164
if isinstance(expected, list):
156-
assert optional_symbols_list_to_list(symbols, stype) == expected
165+
assert symbols_list_to_list(symbols, stype) == expected
157166
else:
158167
with pytest.raises(expected):
159-
optional_symbols_list_to_list(symbols, stype)
168+
symbols_list_to_list(symbols, stype)
160169

161170

162171
@pytest.mark.parametrize(
163172
"symbols, stype, expected",
164173
[
174+
pytest.param(None, SType.RAW_SYMBOL, TypeError),
175+
pytest.param(["NVDA", None], SType.RAW_SYMBOL, TypeError),
176+
pytest.param(["NVDA", [None]], SType.RAW_SYMBOL, TypeError),
165177
pytest.param("NVDA", SType.RAW_SYMBOL, ["NVDA"]),
166178
pytest.param(" nvda ", SType.RAW_SYMBOL, ["NVDA"]),
167179
pytest.param("NVDA,amd", SType.RAW_SYMBOL, ["NVDA", "AMD"]),
@@ -179,7 +191,7 @@ def test_optional_symbols_list_to_list_numpy(
179191
pytest.param(["NVDA", [""]], SType.RAW_SYMBOL, ValueError),
180192
],
181193
)
182-
def test_optional_symbols_list_to_list_raw_symbol(
194+
def test_symbols_list_to_list_raw_symbol(
183195
symbols: list[int] | int | None,
184196
stype: SType,
185197
expected: list[object] | type[Exception],
@@ -188,6 +200,35 @@ def test_optional_symbols_list_to_list_raw_symbol(
188200
Test that str are allowed for SType.RAW_SYMBOL.
189201
"""
190202
# Arrange, Act, Assert
203+
if isinstance(expected, list):
204+
assert symbols_list_to_list(symbols, stype) == expected
205+
else:
206+
with pytest.raises(expected):
207+
symbols_list_to_list(symbols, stype)
208+
209+
210+
@pytest.mark.parametrize(
211+
"symbols, stype, expected",
212+
[
213+
pytest.param(None, SType.RAW_SYMBOL, [ALL_SYMBOLS]),
214+
pytest.param(["NVDA", None], SType.RAW_SYMBOL, TypeError),
215+
pytest.param([12345, None], SType.INSTRUMENT_ID, TypeError),
216+
pytest.param("NVDA", SType.RAW_SYMBOL, ["NVDA"]),
217+
pytest.param(["NVDA", "TSLA"], SType.RAW_SYMBOL, ["NVDA", "TSLA"]),
218+
pytest.param(12345, SType.INSTRUMENT_ID, ["12345"]),
219+
pytest.param([12345, "67890"], SType.INSTRUMENT_ID, ["12345", "67890"]),
220+
],
221+
)
222+
def test_optional_symbols_list_to_list_raw_symbol(
223+
symbols: list[int | str] | int | str | None,
224+
stype: SType,
225+
expected: list[object] | type[Exception],
226+
) -> None:
227+
"""
228+
Test an optional symbols list converts a value of `None` to `[ALL_SYMBOLS]`
229+
and handles other symbols lists.
230+
"""
231+
# Arrange, Act, Assert
191232
if isinstance(expected, list):
192233
assert optional_symbols_list_to_list(symbols, stype) == expected
193234
else:

tests/test_live_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def test_live_async_iteration_after_start(
444444
[
445445
pytest.param("NVDA", id="str"),
446446
pytest.param("ES,CL", id="str-list"),
447-
pytest.param(None, id="all-symbols"),
447+
pytest.param(ALL_SYMBOLS, id="all-symbols"),
448448
],
449449
)
450450
@pytest.mark.parametrize(
@@ -459,7 +459,7 @@ async def test_live_subscribe(
459459
mock_live_server: MockLiveServerInterface,
460460
schema: Schema,
461461
stype_in: SType,
462-
symbols: str | None,
462+
symbols: str,
463463
start: str,
464464
) -> None:
465465
"""

0 commit comments

Comments
 (0)