From 9dbb46fbb1e9949b99616f7aab8e4be06eb2b173 Mon Sep 17 00:00:00 2001 From: Daichi Narushima <1938249+dceoy@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:03:24 +0900 Subject: [PATCH] feat: make pyarrow optional via mt5cli[parquet] extra (#69) --- README.md | 6 ++++++ docs/index.md | 6 ++++++ mt5cli/utils.py | 9 +++++++++ pyproject.toml | 7 +++++-- tests/test_contracts.py | 22 ++++++++++++++++++++++ tests/test_utils.py | 12 ++++++++++++ uv.lock | 11 +++++++++-- 7 files changed, 69 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2f8ec4b..5bfbcd7 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ Built on top of [pdmt5](https://github.com/dceoy/pdmt5), a pandas-based data han pip install -U mt5cli MetaTrader5 ``` +Parquet export is not included by default. To enable it, install the `parquet` extra: + +```bash +pip install -U "mt5cli[parquet]" MetaTrader5 +``` + ## Python API (downstream packages) Import `MT5Client` for generic MT5 data access, schema normalization, and optional order primitives. diff --git a/docs/index.md b/docs/index.md index 457e2cf..6abe618 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,6 +27,12 @@ mt5cli provides a stable `MT5Client` Python API, standardized dataset schemas, s pip install mt5cli ``` +Parquet export is not included by default. To enable it, install the `parquet` extra: + +```bash +pip install "mt5cli[parquet]" +``` + ## Python API for downstream packages Import `MT5Client` for generic MT5 data access, schema normalization, and optional order primitives. diff --git a/mt5cli/utils.py b/mt5cli/utils.py index 40ae584..cf60e7e 100644 --- a/mt5cli/utils.py +++ b/mt5cli/utils.py @@ -314,6 +314,7 @@ def export_dataframe( table_name: Table name for SQLite3 output. Raises: + ImportError: If the parquet format is requested but pyarrow is not installed. ValueError: If the output format is not supported. """ if output_format == "csv": @@ -326,6 +327,14 @@ def export_dataframe( indent=2, ) elif output_format == "parquet": + try: + __import__("pyarrow") + except ImportError as exc: + msg = ( + "Parquet export requires the optional dependency pyarrow. " + 'Install it with: pip install "mt5cli[parquet]"' + ) + raise ImportError(msg) from exc df.to_parquet(output_path, index=False) elif output_format == "sqlite3": export_dataframe_to_sqlite( diff --git a/pyproject.toml b/pyproject.toml index 6e04326..1f85b56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mt5cli" -version = "0.9.6" +version = "0.9.7" description = "Generic MT5 data and execution infrastructure for Python applications" authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}] maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}] @@ -11,7 +11,6 @@ requires-python = ">= 3.11, < 3.14" dependencies = [ "pdmt5>=0.3.0", "click >= 8.1.0", - "pyarrow >= 19.0.0", "typer >= 0.15.0", ] classifiers = [ @@ -25,6 +24,9 @@ classifiers = [ "Topic :: Office/Business :: Financial :: Investment", ] +[project.optional-dependencies] +parquet = ["pyarrow >= 19.0.0"] + [project.scripts] mt5cli = "mt5cli.cli:main" @@ -42,6 +44,7 @@ dev = [ "pytest-mock >= 3.12.0", "pytest-cov >= 5.0.0", "pandas-stubs >= 2.2.3.250527", + "pyarrow >= 19.0.0", "mkdocs >= 1.6.1", "mkdocs-material >= 9.7.6", "mkdocstrings[python] >= 1.0.4", diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 2fbc424..f09a729 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -5,6 +5,7 @@ from __future__ import annotations import re import sqlite3 from datetime import UTC, datetime +from importlib.metadata import requires from pathlib import Path from typing import get_type_hints from unittest.mock import MagicMock @@ -832,3 +833,24 @@ class TestStableSdkContract: assert result.index.tz is not None assert "time" not in result.columns assert "close" in result.columns + + +# --------------------------------------------------------------------------- +# Packaging metadata +# --------------------------------------------------------------------------- + + +def test_parquet_extra_declares_pyarrow() -> None: + """Package metadata lists pyarrow under the parquet optional extra.""" + reqs = requires("mt5cli") or [] + parquet_reqs = [r for r in reqs if "pyarrow" in r and "parquet" in r] + assert parquet_reqs, "pyarrow not found in parquet optional extra" + + +def test_pyarrow_not_in_core_dependencies() -> None: + """Pyarrow is not a core dependency; it belongs only in the parquet extra.""" + reqs = requires("mt5cli") or [] + core_reqs = [r for r in reqs if "extra ==" not in r] + assert not any("pyarrow" in r for r in core_reqs), ( + "pyarrow should not appear in core dependencies" + ) diff --git a/tests/test_utils.py b/tests/test_utils.py index a59fb04..3b2968f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,6 +4,7 @@ from __future__ import annotations import json import sqlite3 +import sys from datetime import UTC, datetime from typing import TYPE_CHECKING @@ -111,6 +112,17 @@ class TestExportDataframe: result = pd.read_parquet(output) pd.testing.assert_frame_equal(result, sample_df) + def test_export_parquet_without_pyarrow( + self, + tmp_path: Path, + sample_df: pd.DataFrame, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Test that a clear error is raised when pyarrow is not installed.""" + monkeypatch.setitem(sys.modules, "pyarrow", None) + with pytest.raises(ImportError, match="mt5cli\\[parquet\\]"): + export_dataframe(sample_df, tmp_path / "out.parquet", "parquet") + def test_export_sqlite3(self, tmp_path: Path, sample_df: pd.DataFrame) -> None: """Test SQLite3 export.""" output = tmp_path / "out.db" diff --git a/uv.lock b/uv.lock index 17152b0..40cd74b 100644 --- a/uv.lock +++ b/uv.lock @@ -492,16 +492,21 @@ source = { editable = "." } dependencies = [ { name = "click" }, { name = "pdmt5" }, - { name = "pyarrow" }, { name = "typer" }, ] +[package.optional-dependencies] +parquet = [ + { name = "pyarrow" }, +] + [package.dev-dependencies] dev = [ { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, { name = "pandas-stubs" }, + { name = "pyarrow" }, { name = "pymdown-extensions" }, { name = "pyright" }, { name = "pytest" }, @@ -514,9 +519,10 @@ dev = [ requires-dist = [ { name = "click", specifier = ">=8.1.0" }, { name = "pdmt5", specifier = ">=0.3.0" }, - { name = "pyarrow", specifier = ">=19.0.0" }, + { name = "pyarrow", marker = "extra == 'parquet'", specifier = ">=19.0.0" }, { name = "typer", specifier = ">=0.15.0" }, ] +provides-extras = ["parquet"] [package.metadata.requires-dev] dev = [ @@ -524,6 +530,7 @@ dev = [ { name = "mkdocs-material", specifier = ">=9.7.6" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.4" }, { name = "pandas-stubs", specifier = ">=2.2.3.250527" }, + { name = "pyarrow", specifier = ">=19.0.0" }, { name = "pymdown-extensions", specifier = ">=10.21.2" }, { name = "pyright", specifier = ">=1.1.407" }, { name = "pytest", specifier = ">=9.0.3" },