Skip to content

Commit ba65a6d

Browse files
authored
Make pandas, numpy, and matplotlib optional dependencies with lazy loading and helpful error messages (#309)
Lazy numpy import Optional dependencies via extras_require Use-case oriented installation extras Helpful error messages Test improvements Version bump to 0.7.0
1 parent 5d3ca01 commit ba65a6d

File tree

9 files changed

+162
-9
lines changed

9 files changed

+162
-9
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ To install the latest release:
1111

1212
`pip install prometheus-api-client`
1313

14+
To install with all optional dependencies (pandas, numpy, matplotlib):
15+
16+
`pip install prometheus-api-client[all]`
17+
18+
**Note:** Starting from version 0.7.0, pandas, numpy, and matplotlib are optional dependencies.
19+
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.
20+
21+
To install only specific extras:
22+
- For DataFrame support: `pip install prometheus-api-client[dataframe]`
23+
- For analytics/aggregation operations: `pip install prometheus-api-client[analytics]`
24+
- For plotting support: `pip install prometheus-api-client[plot]`
25+
1426
To install directly from this branch:
1527

1628
`pip install https://github.com/4n4nd/prometheus-api-client-python/zipball/master`

prometheus_api_client/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""A collection of tools to collect and manipulate prometheus metrics."""
22

33
__title__ = "prometheus-connect"
4-
__version__ = "0.6.0"
4+
__version__ = "0.7.0"
55

66
from .exceptions import PrometheusApiClientException, MetricValueConversionError
77
def __getattr__(name):

prometheus_api_client/metric.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
"""A Class for metric object."""
22
from copy import deepcopy
33
import datetime
4-
import pandas
4+
5+
try:
6+
import pandas
7+
except ImportError as e:
8+
raise ImportError(
9+
"Pandas is required for Metric class. "
10+
"Please install it with: pip install prometheus-api-client[dataframe] "
11+
"or pip install prometheus-api-client[all]"
12+
) from e
513

614
from prometheus_api_client.exceptions import MetricValueConversionError
715

prometheus_api_client/metric_range_df.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
"""A pandas.DataFrame subclass for Prometheus range vector responses."""
2-
from pandas import DataFrame, to_datetime
3-
from pandas._typing import Axes, Dtype
2+
try:
3+
from pandas import DataFrame, to_datetime
4+
from pandas._typing import Axes, Dtype
5+
except ImportError as e:
6+
raise ImportError(
7+
"Pandas is required for MetricRangeDataFrame class. "
8+
"Please install it with: pip install prometheus-api-client[dataframe] "
9+
"or pip install prometheus-api-client[all]"
10+
) from e
11+
412
from typing import Optional, Sequence
513

614
from prometheus_api_client.exceptions import MetricValueConversionError

prometheus_api_client/metric_snapshot_df.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
"""A pandas.DataFrame subclass for Prometheus query response."""
2-
from pandas import DataFrame, to_datetime
3-
from pandas._typing import Axes, Dtype
2+
try:
3+
from pandas import DataFrame, to_datetime
4+
from pandas._typing import Axes, Dtype
5+
except ImportError as e:
6+
raise ImportError(
7+
"Pandas is required for MetricSnapshotDataFrame class. "
8+
"Please install it with: pip install prometheus-api-client[dataframe] "
9+
"or pip install prometheus-api-client[all]"
10+
) from e
11+
412
from typing import Optional, Sequence
513

614
from prometheus_api_client.exceptions import MetricValueConversionError

prometheus_api_client/prometheus_connect.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import os
55
import json
66
import logging
7-
import numpy
87
from datetime import datetime, timedelta
98
import requests
109
from requests.adapters import HTTPAdapter
@@ -569,6 +568,15 @@ def get_metric_aggregation(
569568
'max': 6.009373
570569
}
571570
"""
571+
try:
572+
import numpy
573+
except ImportError as e:
574+
raise ImportError(
575+
"NumPy is required for metric aggregation operations. "
576+
"Please install it with: pip install prometheus-api-client[analytics] "
577+
"or pip install prometheus-api-client[all]"
578+
) from e
579+
572580
if not isinstance(operations, list):
573581
raise TypeError("Operations can be only of type list")
574582
if len(operations) == 0:

requirements-core.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
requests
2+
dateparser

setup.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88

99
def get_install_requires():
10-
"""Get requirements from requirements.txt."""
11-
with open("requirements.txt", "r") as requirements_file:
10+
"""Get core requirements from requirements-core.txt."""
11+
with open("requirements-core.txt", "r") as requirements_file:
1212
res = requirements_file.readlines()
1313
return [req.split(" ", maxsplit=1)[0] for req in res if req]
1414

@@ -36,6 +36,13 @@ def get_version():
3636
long_description_content_type="text/markdown",
3737
url="https://github.com/4n4nd/prometheus-api-client-python",
3838
install_requires=get_install_requires(),
39+
extras_require={
40+
"dataframe": ["pandas>=1.4.0"],
41+
"numpy": ["numpy"],
42+
"plot": ["matplotlib"],
43+
"analytics": ["numpy"],
44+
"all": ["pandas>=1.4.0", "numpy", "matplotlib"],
45+
},
3946
packages=setuptools.find_packages(),
4047
package_data={"prometheus-api-client": ["py.typed"]},
4148
tests_require=["httmock"],

tests/test_lazy_imports.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Test lazy imports to ensure pandas/matplotlib are not loaded unnecessarily."""
2+
import unittest
3+
import sys
4+
import subprocess
5+
6+
7+
class TestLazyImports(unittest.TestCase):
8+
"""Test that PrometheusConnect can be imported without loading heavy dependencies."""
9+
10+
def _run_in_subprocess(self, code, fail_map):
11+
"""Run code in a subprocess and check exit codes against fail_map.
12+
13+
Args:
14+
code: Python code to execute in subprocess
15+
fail_map: Dictionary mapping exit codes to error messages
16+
17+
Raises:
18+
AssertionError: If subprocess exits with a code in fail_map or any non-zero code
19+
"""
20+
result = subprocess.run(
21+
[sys.executable, '-c', code],
22+
capture_output=True,
23+
text=True
24+
)
25+
26+
if result.returncode in fail_map:
27+
self.fail(fail_map[result.returncode])
28+
elif result.returncode != 0:
29+
# Include both stdout and stderr for better debugging
30+
output = []
31+
if result.stdout:
32+
output.append(f"stdout: {result.stdout}")
33+
if result.stderr:
34+
output.append(f"stderr: {result.stderr}")
35+
output_str = "\n".join(output) if output else "no output"
36+
self.fail(f"Subprocess failed with code {result.returncode}: {output_str}")
37+
38+
def test_prometheus_connect_import_without_pandas_matplotlib_numpy(self):
39+
"""Test that importing PrometheusConnect doesn't load pandas, matplotlib, or numpy."""
40+
# Run in a subprocess to avoid affecting other tests
41+
code = """
42+
import sys
43+
from prometheus_api_client import PrometheusConnect
44+
45+
# Check that pandas, matplotlib, and numpy are not loaded
46+
pandas_loaded = any(m == 'pandas' or m.startswith('pandas.') for m in sys.modules.keys())
47+
matplotlib_loaded = any(m == 'matplotlib' or m.startswith('matplotlib.') for m in sys.modules.keys())
48+
numpy_loaded = any(m == 'numpy' or m.startswith('numpy.') for m in sys.modules.keys())
49+
50+
if pandas_loaded:
51+
sys.exit(1)
52+
if matplotlib_loaded:
53+
sys.exit(2)
54+
if numpy_loaded:
55+
sys.exit(3)
56+
sys.exit(0)
57+
"""
58+
fail_map = {
59+
1: "pandas should not be loaded when importing PrometheusConnect",
60+
2: "matplotlib should not be loaded when importing PrometheusConnect",
61+
3: "numpy should not be loaded when importing PrometheusConnect",
62+
}
63+
self._run_in_subprocess(code, fail_map)
64+
65+
def test_prometheus_connect_instantiation_without_numpy(self):
66+
"""Test that PrometheusConnect can be instantiated without loading numpy."""
67+
# Run in a subprocess to avoid affecting other tests
68+
code = """
69+
import sys
70+
from prometheus_api_client import PrometheusConnect
71+
72+
pc = PrometheusConnect(url='http://test.local:9090')
73+
74+
# Check that numpy is still not loaded after instantiation
75+
numpy_loaded = any(m == 'numpy' or m.startswith('numpy.') for m in sys.modules.keys())
76+
77+
if numpy_loaded:
78+
sys.exit(1)
79+
if pc is None:
80+
sys.exit(2)
81+
sys.exit(0)
82+
"""
83+
fail_map = {
84+
1: "numpy should not be loaded when instantiating PrometheusConnect",
85+
2: "PrometheusConnect should be instantiated successfully",
86+
}
87+
self._run_in_subprocess(code, fail_map)
88+
89+
def test_metric_import_loads_pandas(self):
90+
"""Test that importing Metric does load pandas (expected behavior)."""
91+
# This test doesn't remove modules, so it won't cause reload issues
92+
from prometheus_api_client import Metric
93+
94+
# Check that pandas is loaded (this is expected for Metric)
95+
pandas_loaded = any(m == 'pandas' or m.startswith('pandas.') for m in sys.modules.keys())
96+
self.assertTrue(pandas_loaded, "pandas should be loaded when importing Metric")
97+
98+
99+
if __name__ == '__main__':
100+
unittest.main()

0 commit comments

Comments
 (0)