Skip to content

Commit b8a1227

Browse files
authored
VER: Release 0.29.0
See release notes.
2 parents 8287c53 + 481242b commit b8a1227

File tree

12 files changed

+181
-53
lines changed

12 files changed

+181
-53
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ jobs:
1313
if: ${{ github.event.workflow_run.conclusion == 'success' }}
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v3
17-
- uses: actions/setup-python@v4
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-python@v5
1818
with:
1919
python-version: "3.10"
2020
- uses: snok/install-poetry@v1

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ jobs:
1515
runs-on: ${{ matrix.os }}
1616

1717
steps:
18-
- uses: actions/checkout@v3
19-
- uses: actions/setup-python@v4
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-python@v5
2020
with:
2121
python-version: ${{ matrix.python-version }}
2222
- uses: snok/install-poetry@v1

CHANGELOG.md

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

3+
## 0.29.0 - 2024-02-13
4+
5+
#### Enhancements
6+
- Added `tz` parameter to `DBNStore.to_df` which will convert all timestamp fields from UTC to a specified timezone when used with `pretty_ts`
7+
8+
#### Bug fixes
9+
- `Live.block_for_close` and `Live.wait_for_close` will now call `Live.stop` when a timeout is reached instead of `Live.terminate` to close the stream more gracefully
10+
311
## 0.28.0 - 2024-02-01
412

513
#### Enhancements

databento/common/dbnstore.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import pandas as pd
2727
import pyarrow as pa
2828
import pyarrow.parquet as pq
29+
import pytz
2930
import zstandard
3031
from databento_dbn import FIXED_PRICE_SCALE
3132
from databento_dbn import Compression
@@ -47,6 +48,7 @@
4748
from databento.common.error import BentoError
4849
from databento.common.symbology import InstrumentMap
4950
from databento.common.types import DBNRecord
51+
from databento.common.types import Default
5052
from databento.common.validation import validate_enum
5153
from databento.common.validation import validate_file_write_path
5254
from databento.common.validation import validate_maybe_enum
@@ -830,6 +832,7 @@ def to_df(
830832
pretty_ts: bool = ...,
831833
map_symbols: bool = ...,
832834
schema: Schema | str | None = ...,
835+
tz: pytz.BaseTzInfo | str = ...,
833836
count: None = ...,
834837
) -> pd.DataFrame:
835838
...
@@ -841,6 +844,7 @@ def to_df(
841844
pretty_ts: bool = ...,
842845
map_symbols: bool = ...,
843846
schema: Schema | str | None = ...,
847+
tz: pytz.BaseTzInfo | str = ...,
844848
count: int = ...,
845849
) -> DataFrameIterator:
846850
...
@@ -851,6 +855,7 @@ def to_df(
851855
pretty_ts: bool = True,
852856
map_symbols: bool = True,
853857
schema: Schema | str | None = None,
858+
tz: pytz.BaseTzInfo | str | Default[pytz.BaseTzInfo] = Default[pytz.BaseTzInfo](pytz.UTC),
854859
count: int | None = None,
855860
) -> pd.DataFrame | DataFrameIterator:
856861
"""
@@ -865,14 +870,16 @@ def to_df(
865870
If "decimal", prices will be instances of `decimal.Decimal`.
866871
pretty_ts : bool, default True
867872
If all timestamp columns should be converted from UNIX nanosecond
868-
`int` to tz-aware UTC `pd.Timestamp`.
873+
`int` to tz-aware `pd.Timestamp`. The timezone can be specified using the `tz` parameter.
869874
map_symbols : bool, default True
870875
If symbology mappings from the metadata should be used to create
871876
a 'symbol' column, mapping the instrument ID to its requested symbol for
872877
every record.
873878
schema : Schema or str, optional
874879
The DBN schema for the dataframe.
875880
This is only required when reading a DBN stream with mixed record types.
881+
tz : pytz.BaseTzInfo or str, default UTC
882+
If `pretty_ts` is `True`, all timestamps will be converted to the specified timezone.
876883
count : int, optional
877884
If set, instead of returning a single `DataFrame` a `DataFrameIterator`
878885
instance will be returned. When iterated, this object will yield
@@ -892,6 +899,14 @@ def to_df(
892899
893900
"""
894901
schema = validate_maybe_enum(schema, Schema, "schema")
902+
903+
if isinstance(tz, Default):
904+
tz = tz.value # consume default
905+
elif not pretty_ts:
906+
raise ValueError("A timezone was specified when `pretty_ts` is `False`. Did you mean to set `pretty_ts=True`?")
907+
908+
if not isinstance(tz, pytz.BaseTzInfo):
909+
tz = pytz.timezone(tz)
895910
if schema is None:
896911
if self.schema is None:
897912
raise ValueError("a schema must be specified for mixed DBN data")
@@ -910,6 +925,7 @@ def to_df(
910925
count=count,
911926
struct_type=self._schema_struct_map[schema],
912927
instrument_map=self._instrument_map,
928+
tz=tz,
913929
price_type=price_type,
914930
pretty_ts=pretty_ts,
915931
map_symbols=map_symbols,
@@ -1334,6 +1350,7 @@ def __init__(
13341350
count: int | None,
13351351
struct_type: type[DBNRecord],
13361352
instrument_map: InstrumentMap,
1353+
tz: pytz.BaseTzInfo,
13371354
price_type: Literal["fixed", "float", "decimal"] = "float",
13381355
pretty_ts: bool = True,
13391356
map_symbols: bool = True,
@@ -1345,6 +1362,7 @@ def __init__(
13451362
self._pretty_ts = pretty_ts
13461363
self._map_symbols = map_symbols
13471364
self._instrument_map = instrument_map
1365+
self._tz = tz
13481366

13491367
def __iter__(self) -> DataFrameIterator:
13501368
return self
@@ -1411,7 +1429,7 @@ def _format_px(
14111429

14121430
def _format_pretty_ts(self, df: pd.DataFrame) -> None:
14131431
for field in self._struct_type._timestamp_fields:
1414-
df[field] = pd.to_datetime(df[field], utc=True, errors="coerce")
1432+
df[field] = pd.to_datetime(df[field], utc=True, errors="coerce").dt.tz_convert(self._tz)
14151433

14161434
def _format_set_index(self, df: pd.DataFrame) -> None:
14171435
index_column = self._struct_type._ordered_fields[0]

databento/common/types.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Callable, Union
1+
from typing import Callable, Generic, TypeVar, Union
22

33
import databento_dbn
44

@@ -21,3 +21,34 @@
2121

2222
RecordCallback = Callable[[DBNRecord], None]
2323
ExceptionCallback = Callable[[Exception], None]
24+
25+
_T = TypeVar("_T")
26+
class Default(Generic[_T]):
27+
"""
28+
A container for a default value. This is to be used when a callable wants
29+
to detect if a default parameter value is being used.
30+
31+
Example
32+
-------
33+
def foo(param=Default[int](10)):
34+
if isinstance(param, Default):
35+
print(f"param={param.value} (default)")
36+
else:
37+
print(f"param={param.value}")
38+
39+
"""
40+
41+
def __init__(self, value: _T):
42+
self._value = value
43+
44+
@property
45+
def value(self) -> _T:
46+
"""
47+
The default value.
48+
49+
Returns
50+
-------
51+
_T
52+
53+
"""
54+
return self._value

databento/live/client.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,7 @@ def subscribe(
463463
stype_in : SType or str, default 'raw_symbol'
464464
The input symbology type to resolve from.
465465
start : str or int, optional
466-
UNIX nanosecond epoch timestamp to start streaming from. Must be
467-
within 24 hours.
466+
UNIX nanosecond epoch timestamp to start streaming from (inclusive), based on `ts_event`. Must be within 24 hours except when requesting the mbo or definition schemas.
468467
469468
Raises
470469
------
@@ -538,6 +537,9 @@ def block_for_close(
538537
Block until the session closes or a timeout is reached. A session will
539538
close after `Live.stop` is called or the remote gateway disconnects.
540539
540+
If a `timeout` is specified, `Live.stop` will be called when the
541+
timeout is reached.
542+
541543
Parameters
542544
----------
543545
timeout : float, optional
@@ -562,8 +564,8 @@ def block_for_close(
562564
loop=Live._loop,
563565
).result(timeout=timeout)
564566
except (futures.TimeoutError, KeyboardInterrupt) as exc:
565-
logger.info("terminating session due to %s", type(exc).__name__)
566-
self.terminate()
567+
logger.info("closing session due to %s", type(exc).__name__)
568+
self.stop()
567569
if isinstance(exc, KeyboardInterrupt):
568570
raise
569571
except BentoError:
@@ -582,6 +584,9 @@ async def wait_for_close(
582584
session will close after `Live.stop` is called or the remote gateway
583585
disconnects.
584586
587+
If a `timeout` is specified, `Live.stop` will be called when the
588+
timeout is reached.
589+
585590
Parameters
586591
----------
587592
timeout : float, optional
@@ -610,8 +615,8 @@ async def wait_for_close(
610615
try:
611616
await asyncio.wait_for(waiter, timeout=timeout)
612617
except (asyncio.TimeoutError, KeyboardInterrupt) as exc:
613-
logger.info("terminating session due to %s", type(exc).__name__)
614-
self.terminate()
618+
logger.info("closing session due to %s", type(exc).__name__)
619+
self.stop()
615620
if isinstance(exc, KeyboardInterrupt):
616621
raise
617622
except BentoError:

databento/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.28.0"
1+
__version__ = "0.29.0"

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "databento"
3-
version = "0.28.0"
3+
version = "0.29.0"
44
description = "Official Python client library for Databento"
55
authors = [
66
"Databento <[email protected]>",
@@ -51,6 +51,7 @@ ruff = "^0.0.291"
5151
types-requests = "^2.30.0.0"
5252
tomli = "^2.0.1"
5353
teamcity-messages = "^1.32"
54+
types-pytz = "^2024.1.0.20240203"
5455

5556
[build-system]
5657
requires = ["poetry-core"]

tests/conftest.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@
88
import random
99
import string
1010
import threading
11-
from collections.abc import AsyncGenerator
1211
from collections.abc import Generator
1312
from collections.abc import Iterable
1413
from typing import Callable
1514

1615
import pytest
17-
import pytest_asyncio
1816
from databento import historical
1917
from databento import live
2018
from databento.common.publishers import Dataset
@@ -200,13 +198,35 @@ def fixture_test_api_key() -> str:
200198
return f"db-{random_str}"
201199

202200

203-
@pytest_asyncio.fixture(name="mock_live_server")
204-
async def fixture_mock_live_server(
201+
@pytest.fixture(name="thread_loop", scope="session")
202+
def fixture_thread_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
203+
"""
204+
Fixture for a threaded event loop.
205+
206+
Yields
207+
------
208+
asyncio.AbstractEventLoop
209+
210+
"""
211+
loop = asyncio.new_event_loop()
212+
thread = threading.Thread(
213+
name="MockLiveServer",
214+
target=loop.run_forever,
215+
args=(),
216+
daemon=True,
217+
)
218+
thread.start()
219+
yield loop
220+
loop.stop()
221+
222+
@pytest.fixture(name="mock_live_server")
223+
def fixture_mock_live_server(
224+
thread_loop: asyncio.AbstractEventLoop,
205225
test_api_key: str,
206226
caplog: pytest.LogCaptureFixture,
207227
unused_tcp_port: int,
208228
monkeypatch: pytest.MonkeyPatch,
209-
) -> AsyncGenerator[MockLiveServer, None]:
229+
) -> Generator[MockLiveServer, None, None]:
210230
"""
211231
Fixture for a MockLiveServer instance.
212232
@@ -229,34 +249,22 @@ async def fixture_mock_live_server(
229249
"CONNECT_TIMEOUT_SECONDS",
230250
1,
231251
)
232-
233-
loop = asyncio.new_event_loop()
234-
thread = threading.Thread(
235-
name="MockLiveServer",
236-
target=loop.run_forever,
237-
args=(),
238-
daemon=True,
239-
)
240-
thread.start()
241-
242252
with caplog.at_level("DEBUG"):
243253
mock_live_server = asyncio.run_coroutine_threadsafe(
244254
coro=MockLiveServer.create(
245255
host="127.0.0.1",
246256
port=unused_tcp_port,
247257
dbn_path=TESTS_ROOT / "data",
248258
),
249-
loop=loop,
259+
loop=thread_loop,
250260
).result()
251261

252262
yield mock_live_server
253263

254-
loop.run_in_executor(
255-
None,
256-
loop.stop,
257-
)
258-
thread.join()
259-
264+
asyncio.run_coroutine_threadsafe(
265+
coro=mock_live_server.stop(),
266+
loop=thread_loop,
267+
).result()
260268

261269
@pytest.fixture(name="historical_client")
262270
def fixture_historical_client(

tests/mock_live_server.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,6 @@ async def stop(self) -> None:
711711
)
712712

713713
self.server.close()
714-
await self.server.wait_closed()
715714

716715

717716
if __name__ == "__main__":

0 commit comments

Comments
 (0)