diff --git a/README.md b/README.md index 97c5111..bce2baa 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,18 @@ To install the latest release: `pip install prometheus-api-client` +To install with all optional dependencies (pandas, numpy, matplotlib): + +`pip install prometheus-api-client[all]` + +**Note:** Starting from version 0.7.0, pandas, numpy, and matplotlib are optional dependencies. +If you only need `PrometheusConnect` without DataFrame support or plotting capabilities, you can install the minimal version which significantly reduces memory footprint and installation time, especially on Alpine-based Docker images. + +To install only specific extras: +- For DataFrame support: `pip install prometheus-api-client[dataframe]` +- For analytics/aggregation operations: `pip install prometheus-api-client[analytics]` +- For plotting support: `pip install prometheus-api-client[plot]` + To install directly from this branch: `pip install https://github.com/4n4nd/prometheus-api-client-python/zipball/master` diff --git a/prometheus_api_client/__init__.py b/prometheus_api_client/__init__.py index 2fbd114..dd72b49 100644 --- a/prometheus_api_client/__init__.py +++ b/prometheus_api_client/__init__.py @@ -1,7 +1,7 @@ """A collection of tools to collect and manipulate prometheus metrics.""" __title__ = "prometheus-connect" -__version__ = "0.6.0" +__version__ = "0.7.0" from .exceptions import PrometheusApiClientException, MetricValueConversionError def __getattr__(name): diff --git a/prometheus_api_client/metric.py b/prometheus_api_client/metric.py index e02832b..adc2d9a 100644 --- a/prometheus_api_client/metric.py +++ b/prometheus_api_client/metric.py @@ -1,7 +1,15 @@ """A Class for metric object.""" from copy import deepcopy import datetime -import pandas + +try: + import pandas +except ImportError as e: + raise ImportError( + "Pandas is required for Metric class. " + "Please install it with: pip install prometheus-api-client[dataframe] " + "or pip install prometheus-api-client[all]" + ) from e from prometheus_api_client.exceptions import MetricValueConversionError diff --git a/prometheus_api_client/metric_range_df.py b/prometheus_api_client/metric_range_df.py index 4b98af9..3ec836f 100644 --- a/prometheus_api_client/metric_range_df.py +++ b/prometheus_api_client/metric_range_df.py @@ -1,6 +1,14 @@ """A pandas.DataFrame subclass for Prometheus range vector responses.""" -from pandas import DataFrame, to_datetime -from pandas._typing import Axes, Dtype +try: + from pandas import DataFrame, to_datetime + from pandas._typing import Axes, Dtype +except ImportError as e: + raise ImportError( + "Pandas is required for MetricRangeDataFrame class. " + "Please install it with: pip install prometheus-api-client[dataframe] " + "or pip install prometheus-api-client[all]" + ) from e + from typing import Optional, Sequence from prometheus_api_client.exceptions import MetricValueConversionError diff --git a/prometheus_api_client/metric_snapshot_df.py b/prometheus_api_client/metric_snapshot_df.py index 2f97a7a..980f02b 100644 --- a/prometheus_api_client/metric_snapshot_df.py +++ b/prometheus_api_client/metric_snapshot_df.py @@ -1,6 +1,14 @@ """A pandas.DataFrame subclass for Prometheus query response.""" -from pandas import DataFrame, to_datetime -from pandas._typing import Axes, Dtype +try: + from pandas import DataFrame, to_datetime + from pandas._typing import Axes, Dtype +except ImportError as e: + raise ImportError( + "Pandas is required for MetricSnapshotDataFrame class. " + "Please install it with: pip install prometheus-api-client[dataframe] " + "or pip install prometheus-api-client[all]" + ) from e + from typing import Optional, Sequence from prometheus_api_client.exceptions import MetricValueConversionError diff --git a/prometheus_api_client/prometheus_connect.py b/prometheus_api_client/prometheus_connect.py index cd24cb4..6f74939 100644 --- a/prometheus_api_client/prometheus_connect.py +++ b/prometheus_api_client/prometheus_connect.py @@ -4,7 +4,6 @@ import os import json import logging -import numpy from datetime import datetime, timedelta import requests from requests.adapters import HTTPAdapter @@ -569,6 +568,15 @@ def get_metric_aggregation( 'max': 6.009373 } """ + try: + import numpy + except ImportError as e: + raise ImportError( + "NumPy is required for metric aggregation operations. " + "Please install it with: pip install prometheus-api-client[analytics] " + "or pip install prometheus-api-client[all]" + ) from e + if not isinstance(operations, list): raise TypeError("Operations can be only of type list") if len(operations) == 0: diff --git a/requirements-core.txt b/requirements-core.txt new file mode 100644 index 0000000..bbfd6bf --- /dev/null +++ b/requirements-core.txt @@ -0,0 +1,2 @@ +requests +dateparser diff --git a/setup.py b/setup.py index 4796b1a..120d9ad 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ def get_install_requires(): - """Get requirements from requirements.txt.""" - with open("requirements.txt", "r") as requirements_file: + """Get core requirements from requirements-core.txt.""" + with open("requirements-core.txt", "r") as requirements_file: res = requirements_file.readlines() return [req.split(" ", maxsplit=1)[0] for req in res if req] @@ -36,6 +36,13 @@ def get_version(): long_description_content_type="text/markdown", url="https://github.com/4n4nd/prometheus-api-client-python", install_requires=get_install_requires(), + extras_require={ + "dataframe": ["pandas>=1.4.0"], + "numpy": ["numpy"], + "plot": ["matplotlib"], + "analytics": ["numpy"], + "all": ["pandas>=1.4.0", "numpy", "matplotlib"], + }, packages=setuptools.find_packages(), package_data={"prometheus-api-client": ["py.typed"]}, tests_require=["httmock"], diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py new file mode 100644 index 0000000..bac80bf --- /dev/null +++ b/tests/test_lazy_imports.py @@ -0,0 +1,100 @@ +"""Test lazy imports to ensure pandas/matplotlib are not loaded unnecessarily.""" +import unittest +import sys +import subprocess + + +class TestLazyImports(unittest.TestCase): + """Test that PrometheusConnect can be imported without loading heavy dependencies.""" + + def _run_in_subprocess(self, code, fail_map): + """Run code in a subprocess and check exit codes against fail_map. + + Args: + code: Python code to execute in subprocess + fail_map: Dictionary mapping exit codes to error messages + + Raises: + AssertionError: If subprocess exits with a code in fail_map or any non-zero code + """ + result = subprocess.run( + [sys.executable, '-c', code], + capture_output=True, + text=True + ) + + if result.returncode in fail_map: + self.fail(fail_map[result.returncode]) + elif result.returncode != 0: + # Include both stdout and stderr for better debugging + output = [] + if result.stdout: + output.append(f"stdout: {result.stdout}") + if result.stderr: + output.append(f"stderr: {result.stderr}") + output_str = "\n".join(output) if output else "no output" + self.fail(f"Subprocess failed with code {result.returncode}: {output_str}") + + def test_prometheus_connect_import_without_pandas_matplotlib_numpy(self): + """Test that importing PrometheusConnect doesn't load pandas, matplotlib, or numpy.""" + # Run in a subprocess to avoid affecting other tests + code = """ +import sys +from prometheus_api_client import PrometheusConnect + +# Check that pandas, matplotlib, and numpy are not loaded +pandas_loaded = any(m == 'pandas' or m.startswith('pandas.') for m in sys.modules.keys()) +matplotlib_loaded = any(m == 'matplotlib' or m.startswith('matplotlib.') for m in sys.modules.keys()) +numpy_loaded = any(m == 'numpy' or m.startswith('numpy.') for m in sys.modules.keys()) + +if pandas_loaded: + sys.exit(1) +if matplotlib_loaded: + sys.exit(2) +if numpy_loaded: + sys.exit(3) +sys.exit(0) +""" + fail_map = { + 1: "pandas should not be loaded when importing PrometheusConnect", + 2: "matplotlib should not be loaded when importing PrometheusConnect", + 3: "numpy should not be loaded when importing PrometheusConnect", + } + self._run_in_subprocess(code, fail_map) + + def test_prometheus_connect_instantiation_without_numpy(self): + """Test that PrometheusConnect can be instantiated without loading numpy.""" + # Run in a subprocess to avoid affecting other tests + code = """ +import sys +from prometheus_api_client import PrometheusConnect + +pc = PrometheusConnect(url='http://test.local:9090') + +# Check that numpy is still not loaded after instantiation +numpy_loaded = any(m == 'numpy' or m.startswith('numpy.') for m in sys.modules.keys()) + +if numpy_loaded: + sys.exit(1) +if pc is None: + sys.exit(2) +sys.exit(0) +""" + fail_map = { + 1: "numpy should not be loaded when instantiating PrometheusConnect", + 2: "PrometheusConnect should be instantiated successfully", + } + self._run_in_subprocess(code, fail_map) + + def test_metric_import_loads_pandas(self): + """Test that importing Metric does load pandas (expected behavior).""" + # This test doesn't remove modules, so it won't cause reload issues + from prometheus_api_client import Metric + + # Check that pandas is loaded (this is expected for Metric) + pandas_loaded = any(m == 'pandas' or m.startswith('pandas.') for m in sys.modules.keys()) + self.assertTrue(pandas_loaded, "pandas should be loaded when importing Metric") + + +if __name__ == '__main__': + unittest.main()