diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2bcd06..d85c909 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,10 +13,10 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - uses: snok/install-poetry@v1 with: virtualenvs-create: true @@ -32,7 +32,7 @@ jobs: sed -n '/^## /{n; :a; /^## /q; p; n; ba}' CHANGELOG.md >> NOTES.md - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: fail_on_unmatched_files: true append_body: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9323965..1da37e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,12 +10,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] name: build - Python ${{ matrix.python-version }} (${{ matrix.os }}) runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb13e3..f8dfa2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.65.0 - 2025-11-11 + +#### Deprecations +- Deprecated `mode` parameter in `metadata.get_cost`, which will be removed in a future release + +#### Enhancements +- Added export of `CBBOMsg` and `BBOMsg` from `databento_dbn` to the root `databento` package +- Upgraded `databento-dbn` to 0.43.0 + - Added export of `F_PUBLISHER_SPECIFIC` constant to Python + - Added explicit `Unset` variant for `SystemCode` and `ErrorCode` + - Changed Python getters for enum fields to return the underlying type when no known variant can be found. As a result, these getters no longer raise an exception + +#### Breaking changes +- Removed support for Python 3.9 due to end of life + ## 0.64.0 - 2025-09-30 #### Enhancements @@ -7,6 +22,10 @@ - Added `ts_index` and `pretty_ts_index` properties for records in Python which provides the timestamp that is most appropriate for indexing - Fixed type stub for `channel_id` to allow None +#### Enhancements +- Reduced the log level of `SystemMsg` records in the `Live` client to debug +- Increased the log level of `SystemMsg` records with the code `SystemCode.SLOW_READER_WARNING` to warning + #### Bug fixes - Fixed type hint for `start` parameter in `Live.subscribe()` diff --git a/README.md b/README.md index 0c92658..643e4dc 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,11 @@ You can find our full client API reference on the [Historical Reference](https:/ [Examples](https://databento.com/docs/examples?historical=python&live=python) section for various tutorials and code samples. ## Requirements -The library is fully compatible with the latest distribution of Anaconda 3.9 and above. +The library is fully compatible with distributions of Anaconda 2023.x and above. The minimum dependencies as found in the `pyproject.toml` are also listed below: -- python = "^3.9" +- python = "^3.10" - aiohttp = "^3.8.3" -- databento-dbn = "~0.42.0" +- databento-dbn = "~0.43.0" - numpy = ">=1.23.5" - pandas = ">=1.5.3" - pip-system-certs = ">=4.0" (Windows only) diff --git a/databento/__init__.py b/databento/__init__.py index 07f9455..4d61a50 100644 --- a/databento/__init__.py +++ b/databento/__init__.py @@ -8,7 +8,9 @@ from databento_dbn import UNDEF_STAT_QUANTITY from databento_dbn import UNDEF_TIMESTAMP from databento_dbn import Action +from databento_dbn import BBOMsg from databento_dbn import BidAskPair +from databento_dbn import CBBOMsg from databento_dbn import CMBP1Msg from databento_dbn import Compression from databento_dbn import ConsolidatedBidAskPair @@ -90,6 +92,7 @@ "Action", "BBO1MMsg", "BBO1SMsg", + "BBOMsg", "BentoClientError", "BentoError", "BentoHttpError", @@ -97,6 +100,7 @@ "BidAskPair", "CBBO1MMsg", "CBBO1SMsg", + "CBBOMsg", "CMBP1Msg", "Compression", "ConsolidatedBidAskPair", diff --git a/databento/common/dbnstore.py b/databento/common/dbnstore.py index edb2e52..f4e572a 100644 --- a/databento/common/dbnstore.py +++ b/databento/common/dbnstore.py @@ -7,6 +7,7 @@ import logging import warnings import zoneinfo +from collections.abc import Callable from collections.abc import Generator from collections.abc import Iterator from collections.abc import Mapping @@ -18,7 +19,6 @@ from typing import TYPE_CHECKING from typing import Any from typing import BinaryIO -from typing import Callable from typing import Final from typing import Literal from typing import Protocol diff --git a/databento/common/enums.py b/databento/common/enums.py index a8be618..c490476 100644 --- a/databento/common/enums.py +++ b/databento/common/enums.py @@ -1,10 +1,10 @@ from __future__ import annotations +from collections.abc import Callable from enum import Enum from enum import Flag from enum import IntFlag from enum import unique -from typing import Callable from typing import TypeVar diff --git a/databento/common/parsing.py b/databento/common/parsing.py index 7a3f445..5ec188c 100644 --- a/databento/common/parsing.py +++ b/databento/common/parsing.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from collections.abc import Iterable from datetime import date from datetime import datetime @@ -277,13 +276,13 @@ def datetime_to_string(value: pd.Timestamp | datetime | date | str | int) -> str return pd.to_datetime(value).isoformat() -def date_to_string(value: pd.Timestamp | date | str | int) -> str: +def date_to_string(value: date | str) -> str: """ Return a valid date string from the given value. Parameters ---------- - value : pd.Timestamp, date, str, or int + value : date or str The value to parse. Returns @@ -293,32 +292,10 @@ def date_to_string(value: pd.Timestamp | date | str | int) -> str: """ if isinstance(value, str): return value - elif isinstance(value, date): + elif type(value) is date: return value.isoformat() - elif isinstance(value, int): - warnings.warn( - "Passing an int to `start_date` or `end_date` is deprecated and will be removed in v0.59.0." - "Use a date or str instead.", - DeprecationWarning, - stacklevel=2, - ) - return str(value) - elif isinstance(value, pd.Timestamp): - warnings.warn( - "Passing a pandas Timestamp to `start_date` or `end_date` is deprecated and will be removed in v0.59.0." - "Use a date or str instead.", - DeprecationWarning, - stacklevel=2, - ) - return pd.to_datetime(value).date().isoformat() else: - warnings.warn( - f"Passing a {type(value)} to `start_date` or `end_date` is deprecated and will be removed in v0.59.0." - "Use a date or str instead.", - DeprecationWarning, - stacklevel=2, - ) - return pd.to_datetime(value).date().isoformat() + raise TypeError(f"`{type(value)} is not supported. Only `date` and `str` are supported.") def optional_datetime_to_string( diff --git a/databento/common/types.py b/databento/common/types.py index 55214c5..3dfca45 100644 --- a/databento/common/types.py +++ b/databento/common/types.py @@ -1,37 +1,36 @@ import datetime as dt -from typing import Callable +from collections.abc import Callable from typing import Generic from typing import TypedDict from typing import TypeVar -from typing import Union import databento_dbn import pandas as pd -DBNRecord = Union[ - databento_dbn.BBOMsg, - databento_dbn.CBBOMsg, - databento_dbn.CMBP1Msg, - databento_dbn.MBOMsg, - databento_dbn.MBP1Msg, - databento_dbn.MBP10Msg, - databento_dbn.TradeMsg, - databento_dbn.OHLCVMsg, - databento_dbn.ImbalanceMsg, - databento_dbn.InstrumentDefMsg, - databento_dbn.InstrumentDefMsgV1, - databento_dbn.InstrumentDefMsgV2, - databento_dbn.StatMsg, - databento_dbn.StatMsgV1, - databento_dbn.StatusMsg, - databento_dbn.SymbolMappingMsg, - databento_dbn.SymbolMappingMsgV1, - databento_dbn.SystemMsg, - databento_dbn.SystemMsgV1, - databento_dbn.ErrorMsg, - databento_dbn.ErrorMsgV1, -] +DBNRecord = ( + databento_dbn.BBOMsg + | databento_dbn.CBBOMsg + | databento_dbn.CMBP1Msg + | databento_dbn.MBOMsg + | databento_dbn.MBP1Msg + | databento_dbn.MBP10Msg + | databento_dbn.TradeMsg + | databento_dbn.OHLCVMsg + | databento_dbn.ImbalanceMsg + | databento_dbn.InstrumentDefMsg + | databento_dbn.InstrumentDefMsgV1 + | databento_dbn.InstrumentDefMsgV2 + | databento_dbn.StatMsg + | databento_dbn.StatMsgV1 + | databento_dbn.StatusMsg + | databento_dbn.SymbolMappingMsg + | databento_dbn.SymbolMappingMsgV1 + | databento_dbn.SystemMsg + | databento_dbn.SystemMsgV1 + | databento_dbn.ErrorMsg + | databento_dbn.ErrorMsgV1 +) RecordCallback = Callable[[DBNRecord], None] ExceptionCallback = Callable[[Exception], None] diff --git a/databento/historical/api/metadata.py b/databento/historical/api/metadata.py index 4fcf6f4..112cdc3 100644 --- a/databento/historical/api/metadata.py +++ b/databento/historical/api/metadata.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from collections.abc import Iterable from datetime import date from datetime import datetime @@ -19,6 +20,7 @@ from databento.common.parsing import optional_datetime_to_string from databento.common.parsing import optional_symbols_list_to_list from databento.common.publishers import Dataset +from databento.common.types import Default from databento.common.validation import validate_enum from databento.common.validation import validate_semantic_string @@ -399,7 +401,7 @@ def get_cost( dataset: Dataset | str, start: pd.Timestamp | datetime | date | str | int, end: pd.Timestamp | datetime | date | str | int | None = None, - mode: FeedMode | str = "historical-streaming", + mode: FeedMode | str | Default[None] = Default[None](None), symbols: Iterable[str | int] | str | int | None = None, schema: Schema | str = "trades", stype_in: SType | str = "raw_symbol", @@ -425,8 +427,8 @@ def get_cost( Assumes UTC as timezone unless otherwise specified. If an integer is passed, then this represents nanoseconds since the UNIX epoch. Defaults to the forward filled value of `start` based on the resolution provided. - mode : FeedMode or str {'live', 'historical-streaming', 'historical'}, default 'historical-streaming' - The data feed mode for the request. + mode : FeedMode or str {'live', 'historical-streaming', 'historical'}, default `None` + The data feed mode for the request. This parameter has been deprecated. symbols : Iterable[str | int] or str or int, optional The instrument symbols to filter for. Takes up to 2,000 symbols per request. If 'ALL_SYMBOLS' or `None` then will select **all** symbols. @@ -443,6 +445,13 @@ def get_cost( The cost in US dollars. """ + if not isinstance(mode, Default): + warnings.warn( + "The `mode` parameter is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) + stype_in_valid = validate_enum(stype_in, SType, "stype_in") symbols_list = optional_symbols_list_to_list(symbols, stype_in_valid) data: dict[str, str | None] = { @@ -453,7 +462,6 @@ def get_cost( "schema": str(validate_enum(schema, Schema, "schema")), "stype_in": str(stype_in_valid), "stype_out": str(SType.INSTRUMENT_ID), - "mode": validate_enum(mode, FeedMode, "mode"), } if limit is not None: diff --git a/databento/live/client.py b/databento/live/client.py index 768ef89..fae8c3c 100644 --- a/databento/live/client.py +++ b/databento/live/client.py @@ -392,7 +392,7 @@ def start( logger.info("starting live client") if not self.is_connected(): if self.dataset == "": - raise ValueError("cannot start a live client without a subscription") + raise ValueError("must call subscribe() before starting live client") raise ValueError("cannot start a live client after it is closed") if self._session.is_streaming(): raise ValueError("client is already started") diff --git a/databento/live/protocol.py b/databento/live/protocol.py index 98194ea..372729e 100644 --- a/databento/live/protocol.py +++ b/databento/live/protocol.py @@ -7,9 +7,11 @@ from typing import Final import databento_dbn +from databento_dbn import DBNError from databento_dbn import Metadata from databento_dbn import Schema from databento_dbn import SType +from databento_dbn import SystemCode from databento_dbn import VersionUpgradePolicy from databento.common import cram @@ -380,10 +382,19 @@ def _process_dbn(self, data: bytes) -> None: if record.is_heartbeat(): logger.debug("gateway heartbeat") else: - logger.info( - "gateway message: %s", - record.msg, - ) + try: + msg_code = record.code + except DBNError: + msg_code = None + if msg_code == SystemCode.SLOW_READER_WARNING: + logger.warning( + record.msg, + ) + else: + logger.debug( + "gateway message: %s", + record.msg, + ) self.received_record(record) def _process_gateway(self, data: bytes) -> None: diff --git a/databento/version.py b/databento/version.py index d720aed..e082e13 100644 --- a/databento/version.py +++ b/databento/version.py @@ -1 +1 @@ -__version__ = "0.64.0" +__version__ = "0.65.0" diff --git a/pyproject.toml b/pyproject.toml index d70b6fb..bf1388a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,11 @@ -[tool.poetry] +[project] name = "databento" -version = "0.64.0" +version = "0.65.0" description = "Official Python client library for Databento" -authors = [ - "Databento ", -] -license = "Apache License 2.0" -packages = [ - {include = "databento"}, - {include = "databento/py.typed"}, -] +readme = "README.md" +requires-python = ">=3.10" +license = "Apache-2.0" +authors = [{ name = "Databento", email = "support@databento.com" }] classifiers = [ "Development Status :: 4 - Beta", "Operating System :: OS Independent", @@ -18,30 +14,34 @@ classifiers = [ "Topic :: Office/Business :: Financial", "Topic :: Office/Business :: Financial :: Investment", ] -readme = "README.md" -documentation = "https://databento.com/docs" -homepage = "https://databento.com" -repository = "https://github.com/databento/databento-python" +dependencies = [ + "aiohttp>=3.8.3,<4.0.0; python_version < '3.12'", + "aiohttp>=3.9.0,<4.0.0; python_version >= '3.12'", + "databento-dbn~=0.43.0", + "numpy>=1.23.5; python_version < '3.12'", + "numpy>=1.26.0; python_version >= '3.12'", + "pandas>=1.5.3", + "pip-system-certs>=4.0; platform_system == 'Windows'", + "pyarrow>=13.0.0", + "requests>=2.27.0", + "zstandard>=0.21.0", +] -[tool.poetry.urls] +[project.urls] +Homepage = "https://databento.com" +Documentation = "https://databento.com/docs" +Repository = "https://github.com/databento/databento-python" "Bug Tracker" = "https://github.com/databento/databento-python/issues" -[tool.poetry.dependencies] -python = "^3.9" -aiohttp = [ - {version = "^3.8.3", python = "<3.12"}, - {version = "^3.9.0", python = "^3.12"} -] -databento-dbn = "~=0.42.0" -numpy = [ - {version = ">=1.23.5", python = "<3.12"}, - {version = ">=1.26.0", python = "^3.12"} +[tool.poetry] +requires-poetry = ">=2.0" +packages = [ + { include = "databento" }, + { include = "databento/py.typed" }, ] -pandas = ">=1.5.3" -pip-system-certs = {version=">=4.0", markers="platform_system == 'Windows'"} -pyarrow = ">=13.0.0" -requests = ">=2.27.0" -zstandard = ">=0.21.0" + +[tool.poetry.dependencies] +python = ">=3.10,<3.14" [tool.poetry.group.dev.dependencies] black = "^23.9.1" @@ -63,7 +63,7 @@ build-backend = "poetry.core.masonry.api" line_length = 100 [tool.mypy] -python_version = 3.9 +python_version = "3.10" disallow_untyped_defs = true disallow_any_generics = true disallow_subclassing_any = true @@ -78,3 +78,6 @@ plugins = ["numpy.typing.mypy_plugin"] [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" + +[tool.ruff] +target-version = "py310" diff --git a/tests/conftest.py b/tests/conftest.py index 8c01b4e..94fb5d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,9 +8,9 @@ import random import string from collections.abc import AsyncGenerator +from collections.abc import Callable from collections.abc import Generator from collections.abc import Iterable -from typing import Callable import databento.live.session import pytest diff --git a/tests/mockliveserver/fixture.py b/tests/mockliveserver/fixture.py index 6bada8d..802591a 100644 --- a/tests/mockliveserver/fixture.py +++ b/tests/mockliveserver/fixture.py @@ -5,8 +5,8 @@ import sys from asyncio.subprocess import Process from collections.abc import AsyncGenerator +from collections.abc import Callable from collections.abc import Generator -from typing import Callable from typing import TypeVar import pytest diff --git a/tests/test_bento_data_source.py b/tests/test_bento_data_source.py index 435e79b..c758b12 100644 --- a/tests/test_bento_data_source.py +++ b/tests/test_bento_data_source.py @@ -1,5 +1,5 @@ import pathlib -from typing import Callable +from collections.abc import Callable import pytest from databento.common.dbnstore import FileDataSource diff --git a/tests/test_common_symbology.py b/tests/test_common_symbology.py index 027fbeb..8cd0264 100644 --- a/tests/test_common_symbology.py +++ b/tests/test_common_symbology.py @@ -2,9 +2,9 @@ import json import pathlib +from collections.abc import Callable from collections.abc import Iterable from collections.abc import Sequence -from typing import Callable from typing import NamedTuple import databento_dbn diff --git a/tests/test_historical_bento.py b/tests/test_historical_bento.py index 5194a5a..674d298 100644 --- a/tests/test_historical_bento.py +++ b/tests/test_historical_bento.py @@ -2,10 +2,10 @@ import datetime as dt import decimal import zoneinfo +from collections.abc import Callable from io import BytesIO from pathlib import Path from typing import Any -from typing import Callable from typing import Literal from unittest.mock import MagicMock @@ -561,7 +561,7 @@ def test_to_df_with_pretty_ts_converts_timestamps_as_expected( # Assert index0 = df.index[0] - event0 = df["ts_event"][0] + event0 = df["ts_event"].iloc[0] assert isinstance(index0, pd.Timestamp) assert isinstance(event0, pd.Timestamp) assert index0 == pd.Timestamp("2020-12-28 00:00:00.000000000+0000", tz="UTC") diff --git a/tests/test_historical_client.py b/tests/test_historical_client.py index 5a525a2..6e75bc2 100644 --- a/tests/test_historical_client.py +++ b/tests/test_historical_client.py @@ -1,7 +1,7 @@ from __future__ import annotations import pathlib -from typing import Callable +from collections.abc import Callable from unittest.mock import MagicMock import databento as db diff --git a/tests/test_historical_metadata.py b/tests/test_historical_metadata.py index 44b55dd..b7c60d7 100644 --- a/tests/test_historical_metadata.py +++ b/tests/test_historical_metadata.py @@ -292,7 +292,6 @@ def test_get_cost_sends_expected_request( "schema": "mbo", "stype_in": "raw_symbol", "stype_out": "instrument_id", - "mode": "historical-streaming", "limit": "1000000", } assert call["timeout"] == (100, 100) diff --git a/tests/test_historical_timeseries.py b/tests/test_historical_timeseries.py index 1d947d4..6f992da 100644 --- a/tests/test_historical_timeseries.py +++ b/tests/test_historical_timeseries.py @@ -1,5 +1,5 @@ +from collections.abc import Callable from pathlib import Path -from typing import Callable from unittest.mock import MagicMock import databento as db diff --git a/tests/test_live_client.py b/tests/test_live_client.py index f43510c..c8625e9 100644 --- a/tests/test_live_client.py +++ b/tests/test_live_client.py @@ -9,8 +9,8 @@ import platform import random import string +from collections.abc import Callable from io import BytesIO -from typing import Callable from unittest.mock import MagicMock import databento_dbn diff --git a/tests/test_release.py b/tests/test_release.py index 1996bf4..ceb1a57 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -44,7 +44,7 @@ def fixture_pyproject_version() -> str: # Arrange, Act, Assert with open(PROJECT_ROOT / "pyproject.toml", "rb") as pyproject: data = tomli.load(pyproject) - return data["tool"]["poetry"]["version"] + return data["project"]["version"] @pytest.mark.release