From 74b9105a5bacd88b040178ab8b2004126ea9de37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 03:54:21 +0000 Subject: [PATCH 01/13] Initial plan From d6dd79dd90a41c69d1230b1bf4a7a975e28d3478 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 04:00:50 +0000 Subject: [PATCH 02/13] Make numpy import lazy in prometheus_connect.py - Remove numpy import from module level in prometheus_connect.py - Add lazy import of numpy inside get_metric_aggregation method - Add tests to verify PrometheusConnect can be imported without loading pandas/matplotlib/numpy - All mocked network tests pass successfully Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- prometheus_api_client/prometheus_connect.py | 3 +- tests/test_lazy_imports.py | 100 ++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 tests/test_lazy_imports.py diff --git a/prometheus_api_client/prometheus_connect.py b/prometheus_api_client/prometheus_connect.py index cd24cb4..0149930 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,8 @@ def get_metric_aggregation( 'max': 6.009373 } """ + import numpy + if not isinstance(operations, list): raise TypeError("Operations can be only of type list") if len(operations) == 0: diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py new file mode 100644 index 0000000..c0fce18 --- /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 importlib + + +class TestLazyImports(unittest.TestCase): + """Test that PrometheusConnect can be imported without loading heavy dependencies.""" + + def test_prometheus_connect_import_without_pandas_matplotlib_numpy(self): + """Test that importing PrometheusConnect doesn't load pandas, matplotlib, or numpy.""" + # Remove any previously loaded prometheus_api_client modules + modules_to_remove = [ + key for key in sys.modules.keys() + if key.startswith('prometheus_api_client') + ] + for module in modules_to_remove: + del sys.modules[module] + + # Also remove numpy, pandas, matplotlib if they were loaded + for heavy_module in ['numpy', 'pandas', 'matplotlib']: + modules_to_remove = [ + key for key in sys.modules.keys() + if key == heavy_module or key.startswith(heavy_module + '.') + ] + for module in modules_to_remove: + del sys.modules[module] + + # Import PrometheusConnect + from prometheus_api_client import PrometheusConnect + + # Check that pandas, matplotlib, and numpy are not loaded + loaded_modules = sys.modules.keys() + pandas_loaded = any('pandas' in m for m in loaded_modules) + matplotlib_loaded = any('matplotlib' in m for m in loaded_modules) + numpy_loaded = any('numpy' in m for m in loaded_modules) + + self.assertFalse(pandas_loaded, "pandas should not be loaded when importing PrometheusConnect") + self.assertFalse(matplotlib_loaded, "matplotlib should not be loaded when importing PrometheusConnect") + self.assertFalse(numpy_loaded, "numpy should not be loaded when importing PrometheusConnect") + + def test_prometheus_connect_instantiation_without_numpy(self): + """Test that PrometheusConnect can be instantiated without loading numpy.""" + # Remove any previously loaded prometheus_api_client modules + modules_to_remove = [ + key for key in sys.modules.keys() + if key.startswith('prometheus_api_client') + ] + for module in modules_to_remove: + del sys.modules[module] + + # Also remove numpy if it was loaded + modules_to_remove = [ + key for key in sys.modules.keys() + if key == 'numpy' or key.startswith('numpy.') + ] + for module in modules_to_remove: + del sys.modules[module] + + # Import and instantiate PrometheusConnect + from prometheus_api_client import PrometheusConnect + pc = PrometheusConnect(url='http://test.local:9090') + + # Check that numpy is still not loaded after instantiation + loaded_modules = sys.modules.keys() + numpy_loaded = any('numpy' in m for m in loaded_modules) + + self.assertFalse(numpy_loaded, "numpy should not be loaded when instantiating PrometheusConnect") + self.assertIsNotNone(pc, "PrometheusConnect should be instantiated successfully") + + def test_metric_import_loads_pandas(self): + """Test that importing Metric does load pandas (expected behavior).""" + # Remove any previously loaded prometheus_api_client modules + modules_to_remove = [ + key for key in sys.modules.keys() + if key.startswith('prometheus_api_client') + ] + for module in modules_to_remove: + del sys.modules[module] + + # Also remove pandas if it was loaded + modules_to_remove = [ + key for key in sys.modules.keys() + if key == 'pandas' or key.startswith('pandas.') + ] + for module in modules_to_remove: + del sys.modules[module] + + # Import Metric + from prometheus_api_client import Metric + + # Check that pandas is loaded (this is expected for Metric) + loaded_modules = sys.modules.keys() + pandas_loaded = any('pandas' in m for m in loaded_modules) + + self.assertTrue(pandas_loaded, "pandas should be loaded when importing Metric") + + +if __name__ == '__main__': + unittest.main() From 2447357474c0e2e0f78148b573674ffdf0f81a33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 04:03:13 +0000 Subject: [PATCH 03/13] Add optional dependencies support via extras_require - Create requirements-core.txt with only core dependencies (requests, dateparser) - Update setup.py to support extras_require with 'dataframe', 'numpy', 'plot', and 'all' options - Update README.md with installation instructions for optional dependencies - Users can now install minimal version without pandas/matplotlib/numpy for reduced memory footprint Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- README.md | 12 ++++++++++++ requirements-core.txt | 2 ++ setup.py | 10 ++++++++-- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 requirements-core.txt diff --git a/README.md b/README.md index 97c5111..92b185a 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.6.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 numpy support: `pip install prometheus-api-client[numpy]` +- 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/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..4a6c1ab 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,12 @@ 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"], + "all": ["pandas>=1.4.0", "numpy", "matplotlib"], + }, packages=setuptools.find_packages(), package_data={"prometheus-api-client": ["py.typed"]}, tests_require=["httmock"], From 74f87727d6bebf09a48ed8deee22d20b1d2501c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 04:06:24 +0000 Subject: [PATCH 04/13] Fix test module detection logic to avoid false positives - Use exact module name matching instead of substring matching - Check for 'modulename' or 'modulename.*' pattern to avoid false positives - Prevents incorrect detection of modules with similar names Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- tests/test_lazy_imports.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index c0fce18..295063f 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -31,9 +31,9 @@ def test_prometheus_connect_import_without_pandas_matplotlib_numpy(self): # Check that pandas, matplotlib, and numpy are not loaded loaded_modules = sys.modules.keys() - pandas_loaded = any('pandas' in m for m in loaded_modules) - matplotlib_loaded = any('matplotlib' in m for m in loaded_modules) - numpy_loaded = any('numpy' in m for m in loaded_modules) + pandas_loaded = any(m == 'pandas' or m.startswith('pandas.') for m in loaded_modules) + matplotlib_loaded = any(m == 'matplotlib' or m.startswith('matplotlib.') for m in loaded_modules) + numpy_loaded = any(m == 'numpy' or m.startswith('numpy.') for m in loaded_modules) self.assertFalse(pandas_loaded, "pandas should not be loaded when importing PrometheusConnect") self.assertFalse(matplotlib_loaded, "matplotlib should not be loaded when importing PrometheusConnect") @@ -63,7 +63,7 @@ def test_prometheus_connect_instantiation_without_numpy(self): # Check that numpy is still not loaded after instantiation loaded_modules = sys.modules.keys() - numpy_loaded = any('numpy' in m for m in loaded_modules) + numpy_loaded = any(m == 'numpy' or m.startswith('numpy.') for m in loaded_modules) self.assertFalse(numpy_loaded, "numpy should not be loaded when instantiating PrometheusConnect") self.assertIsNotNone(pc, "PrometheusConnect should be instantiated successfully") @@ -91,7 +91,7 @@ def test_metric_import_loads_pandas(self): # Check that pandas is loaded (this is expected for Metric) loaded_modules = sys.modules.keys() - pandas_loaded = any('pandas' in m for m in loaded_modules) + pandas_loaded = any(m == 'pandas' or m.startswith('pandas.') for m in loaded_modules) self.assertTrue(pandas_loaded, "pandas should be loaded when importing Metric") From 33e0289a820066aea2e0a9b4be9b2c17e873e553 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 04:08:18 +0000 Subject: [PATCH 05/13] Refactor test code to reduce duplication - Extract module removal logic into _remove_modules() helper method - Extract module checking logic into _is_module_loaded() helper method - Improve code maintainability and readability Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- tests/test_lazy_imports.py | 97 +++++++++++++++----------------------- 1 file changed, 39 insertions(+), 58 deletions(-) diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index 295063f..7bf9c2b 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -7,93 +7,74 @@ class TestLazyImports(unittest.TestCase): """Test that PrometheusConnect can be imported without loading heavy dependencies.""" - def test_prometheus_connect_import_without_pandas_matplotlib_numpy(self): - """Test that importing PrometheusConnect doesn't load pandas, matplotlib, or numpy.""" - # Remove any previously loaded prometheus_api_client modules - modules_to_remove = [ - key for key in sys.modules.keys() - if key.startswith('prometheus_api_client') - ] - for module in modules_to_remove: - del sys.modules[module] + @staticmethod + def _remove_modules(module_names): + """Remove specified modules and their submodules from sys.modules. - # Also remove numpy, pandas, matplotlib if they were loaded - for heavy_module in ['numpy', 'pandas', 'matplotlib']: + Args: + module_names: List of module names to remove + """ + for module_name in module_names: modules_to_remove = [ key for key in sys.modules.keys() - if key == heavy_module or key.startswith(heavy_module + '.') + if key == module_name or key.startswith(module_name + '.') ] for module in modules_to_remove: del sys.modules[module] + + @staticmethod + def _is_module_loaded(module_name): + """Check if a module is loaded in sys.modules. + + Args: + module_name: Name of the module to check + + Returns: + bool: True if module is loaded, False otherwise + """ + return any(m == module_name or m.startswith(module_name + '.') for m in sys.modules.keys()) + + def test_prometheus_connect_import_without_pandas_matplotlib_numpy(self): + """Test that importing PrometheusConnect doesn't load pandas, matplotlib, or numpy.""" + # Remove any previously loaded modules + self._remove_modules(['prometheus_api_client', 'numpy', 'pandas', 'matplotlib']) # Import PrometheusConnect from prometheus_api_client import PrometheusConnect # Check that pandas, matplotlib, and numpy are not loaded - loaded_modules = sys.modules.keys() - pandas_loaded = any(m == 'pandas' or m.startswith('pandas.') for m in loaded_modules) - matplotlib_loaded = any(m == 'matplotlib' or m.startswith('matplotlib.') for m in loaded_modules) - numpy_loaded = any(m == 'numpy' or m.startswith('numpy.') for m in loaded_modules) - - self.assertFalse(pandas_loaded, "pandas should not be loaded when importing PrometheusConnect") - self.assertFalse(matplotlib_loaded, "matplotlib should not be loaded when importing PrometheusConnect") - self.assertFalse(numpy_loaded, "numpy should not be loaded when importing PrometheusConnect") + self.assertFalse(self._is_module_loaded('pandas'), + "pandas should not be loaded when importing PrometheusConnect") + self.assertFalse(self._is_module_loaded('matplotlib'), + "matplotlib should not be loaded when importing PrometheusConnect") + self.assertFalse(self._is_module_loaded('numpy'), + "numpy should not be loaded when importing PrometheusConnect") def test_prometheus_connect_instantiation_without_numpy(self): """Test that PrometheusConnect can be instantiated without loading numpy.""" - # Remove any previously loaded prometheus_api_client modules - modules_to_remove = [ - key for key in sys.modules.keys() - if key.startswith('prometheus_api_client') - ] - for module in modules_to_remove: - del sys.modules[module] - - # Also remove numpy if it was loaded - modules_to_remove = [ - key for key in sys.modules.keys() - if key == 'numpy' or key.startswith('numpy.') - ] - for module in modules_to_remove: - del sys.modules[module] + # Remove any previously loaded modules + self._remove_modules(['prometheus_api_client', 'numpy']) # Import and instantiate PrometheusConnect from prometheus_api_client import PrometheusConnect pc = PrometheusConnect(url='http://test.local:9090') # Check that numpy is still not loaded after instantiation - loaded_modules = sys.modules.keys() - numpy_loaded = any(m == 'numpy' or m.startswith('numpy.') for m in loaded_modules) - - self.assertFalse(numpy_loaded, "numpy should not be loaded when instantiating PrometheusConnect") + self.assertFalse(self._is_module_loaded('numpy'), + "numpy should not be loaded when instantiating PrometheusConnect") self.assertIsNotNone(pc, "PrometheusConnect should be instantiated successfully") def test_metric_import_loads_pandas(self): """Test that importing Metric does load pandas (expected behavior).""" - # Remove any previously loaded prometheus_api_client modules - modules_to_remove = [ - key for key in sys.modules.keys() - if key.startswith('prometheus_api_client') - ] - for module in modules_to_remove: - del sys.modules[module] - - # Also remove pandas if it was loaded - modules_to_remove = [ - key for key in sys.modules.keys() - if key == 'pandas' or key.startswith('pandas.') - ] - for module in modules_to_remove: - del sys.modules[module] + # Remove any previously loaded modules + self._remove_modules(['prometheus_api_client', 'pandas']) # Import Metric from prometheus_api_client import Metric # Check that pandas is loaded (this is expected for Metric) - loaded_modules = sys.modules.keys() - pandas_loaded = any(m == 'pandas' or m.startswith('pandas.') for m in loaded_modules) - - self.assertTrue(pandas_loaded, "pandas should be loaded when importing Metric") + self.assertTrue(self._is_module_loaded('pandas'), + "pandas should be loaded when importing Metric") if __name__ == '__main__': From 7832b91ea887212aaf30131f97aec00a1deefc32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 04:17:24 +0000 Subject: [PATCH 06/13] Bump version to 0.7.0 for new release - Version 0.6.0 is already released - Increment to 0.7.0 for this new feature release with optional dependencies Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- prometheus_api_client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): From b7f4b1e592a5d073023122b7717ba07b18daa9fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 04:24:31 +0000 Subject: [PATCH 07/13] Update README.md to reflect version 0.7.0 - Changed version reference from 0.6.0 to 0.7.0 in the optional dependencies note Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92b185a..d7ea10b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ To install with all optional dependencies (pandas, numpy, matplotlib): `pip install prometheus-api-client[all]` -**Note:** Starting from version 0.6.0, pandas, numpy, and matplotlib are optional dependencies. +**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: From 08a7642b011e72591aeb5a6368e7ce156698e44c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 04:57:22 +0000 Subject: [PATCH 08/13] Keep pandas as a core dependency - Moved pandas>=1.4.0 from extras_require back to core requirements - Updated README.md to reflect that pandas is a core dependency - Removed 'dataframe' extra (no longer needed) - NumPy remains optional with lazy loading - All tests pass (46/46 relevant tests, same 9 pre-existing failures) Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- README.md | 7 +++---- requirements-core.txt | 1 + setup.py | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d7ea10b..da6c727 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,14 @@ To install the latest release: `pip install prometheus-api-client` -To install with all optional dependencies (pandas, numpy, matplotlib): +To install with all optional dependencies (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. +**Note:** Starting from version 0.7.0, numpy and matplotlib are optional dependencies. Pandas is included as a core dependency. +The lazy import mechanism ensures that `PrometheusConnect` can be imported without loading numpy until `get_metric_aggregation()` is called, which reduces initial memory footprint. To install only specific extras: -- For DataFrame support: `pip install prometheus-api-client[dataframe]` - For numpy support: `pip install prometheus-api-client[numpy]` - For plotting support: `pip install prometheus-api-client[plot]` diff --git a/requirements-core.txt b/requirements-core.txt index bbfd6bf..cc00aef 100644 --- a/requirements-core.txt +++ b/requirements-core.txt @@ -1,2 +1,3 @@ requests dateparser +pandas>=1.4.0 diff --git a/setup.py b/setup.py index 4a6c1ab..3a71d9e 100644 --- a/setup.py +++ b/setup.py @@ -37,10 +37,9 @@ def get_version(): 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"], - "all": ["pandas>=1.4.0", "numpy", "matplotlib"], + "all": ["numpy", "matplotlib"], }, packages=setuptools.find_packages(), package_data={"prometheus-api-client": ["py.typed"]}, From c02129e84a8fe0bfed70d26c51075b675e3c808f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 05:53:07 +0000 Subject: [PATCH 09/13] Fix numpy reload issue in TestLazyImports - Refactored tests to use subprocess isolation instead of removing modules from sys.modules - Prevents numpy from being reloaded which was causing test failures - All 55 tests now pass with no warnings - test_metric_import_loads_pandas no longer removes modules since it just checks loaded state Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- tests/test_lazy_imports.py | 117 ++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 55 deletions(-) diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index 7bf9c2b..00bcf04 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -1,80 +1,87 @@ """Test lazy imports to ensure pandas/matplotlib are not loaded unnecessarily.""" import unittest import sys +import subprocess import importlib class TestLazyImports(unittest.TestCase): """Test that PrometheusConnect can be imported without loading heavy dependencies.""" - @staticmethod - def _remove_modules(module_names): - """Remove specified modules and their submodules from sys.modules. - - Args: - module_names: List of module names to remove - """ - for module_name in module_names: - modules_to_remove = [ - key for key in sys.modules.keys() - if key == module_name or key.startswith(module_name + '.') - ] - for module in modules_to_remove: - del sys.modules[module] - - @staticmethod - def _is_module_loaded(module_name): - """Check if a module is loaded in sys.modules. - - Args: - module_name: Name of the module to check - - Returns: - bool: True if module is loaded, False otherwise - """ - return any(m == module_name or m.startswith(module_name + '.') for m in sys.modules.keys()) - def test_prometheus_connect_import_without_pandas_matplotlib_numpy(self): """Test that importing PrometheusConnect doesn't load pandas, matplotlib, or numpy.""" - # Remove any previously loaded modules - self._remove_modules(['prometheus_api_client', 'numpy', 'pandas', 'matplotlib']) - - # Import PrometheusConnect - from prometheus_api_client import PrometheusConnect + # 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) +""" + result = subprocess.run( + [sys.executable, '-c', code], + capture_output=True, + text=True + ) - # Check that pandas, matplotlib, and numpy are not loaded - self.assertFalse(self._is_module_loaded('pandas'), - "pandas should not be loaded when importing PrometheusConnect") - self.assertFalse(self._is_module_loaded('matplotlib'), - "matplotlib should not be loaded when importing PrometheusConnect") - self.assertFalse(self._is_module_loaded('numpy'), - "numpy should not be loaded when importing PrometheusConnect") + if result.returncode == 1: + self.fail("pandas should not be loaded when importing PrometheusConnect") + elif result.returncode == 2: + self.fail("matplotlib should not be loaded when importing PrometheusConnect") + elif result.returncode == 3: + self.fail("numpy should not be loaded when importing PrometheusConnect") + elif result.returncode != 0: + self.fail(f"Subprocess failed with code {result.returncode}: {result.stderr}") def test_prometheus_connect_instantiation_without_numpy(self): """Test that PrometheusConnect can be instantiated without loading numpy.""" - # Remove any previously loaded modules - self._remove_modules(['prometheus_api_client', 'numpy']) - - # Import and instantiate PrometheusConnect - from prometheus_api_client import PrometheusConnect - pc = PrometheusConnect(url='http://test.local:9090') + # 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) +""" + result = subprocess.run( + [sys.executable, '-c', code], + capture_output=True, + text=True + ) - # Check that numpy is still not loaded after instantiation - self.assertFalse(self._is_module_loaded('numpy'), - "numpy should not be loaded when instantiating PrometheusConnect") - self.assertIsNotNone(pc, "PrometheusConnect should be instantiated successfully") + if result.returncode == 1: + self.fail("numpy should not be loaded when instantiating PrometheusConnect") + elif result.returncode == 2: + self.fail("PrometheusConnect should be instantiated successfully") + elif result.returncode != 0: + self.fail(f"Subprocess failed with code {result.returncode}: {result.stderr}") def test_metric_import_loads_pandas(self): """Test that importing Metric does load pandas (expected behavior).""" - # Remove any previously loaded modules - self._remove_modules(['prometheus_api_client', 'pandas']) - - # Import Metric + # 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) - self.assertTrue(self._is_module_loaded('pandas'), - "pandas should be loaded when importing 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__': From 1c8d53b2e0aaa68f3b2a0b5db79a6f11583041c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:02:58 +0000 Subject: [PATCH 10/13] Remove unused importlib import from test_lazy_imports.py - importlib was imported but never used in the test file - All tests still pass after removal Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- tests/test_lazy_imports.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index 00bcf04..33de601 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -2,7 +2,6 @@ import unittest import sys import subprocess -import importlib class TestLazyImports(unittest.TestCase): From c2db4c46d666ea7beb00120f052dd64f785023b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:08:06 +0000 Subject: [PATCH 11/13] Refactor test_lazy_imports.py to reduce code duplication - Added _run_in_subprocess helper method to eliminate repetition - Helper accepts code and fail_map for flexible error handling - Enhanced error reporting to include both stdout and stderr for better debugging - All 55 tests still pass Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- tests/test_lazy_imports.py | 65 +++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index 33de601..bac80bf 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -7,6 +7,34 @@ 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 @@ -27,20 +55,12 @@ def test_prometheus_connect_import_without_pandas_matplotlib_numpy(self): sys.exit(3) sys.exit(0) """ - result = subprocess.run( - [sys.executable, '-c', code], - capture_output=True, - text=True - ) - - if result.returncode == 1: - self.fail("pandas should not be loaded when importing PrometheusConnect") - elif result.returncode == 2: - self.fail("matplotlib should not be loaded when importing PrometheusConnect") - elif result.returncode == 3: - self.fail("numpy should not be loaded when importing PrometheusConnect") - elif result.returncode != 0: - self.fail(f"Subprocess failed with code {result.returncode}: {result.stderr}") + 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.""" @@ -60,18 +80,11 @@ def test_prometheus_connect_instantiation_without_numpy(self): sys.exit(2) sys.exit(0) """ - result = subprocess.run( - [sys.executable, '-c', code], - capture_output=True, - text=True - ) - - if result.returncode == 1: - self.fail("numpy should not be loaded when instantiating PrometheusConnect") - elif result.returncode == 2: - self.fail("PrometheusConnect should be instantiated successfully") - elif result.returncode != 0: - self.fail(f"Subprocess failed with code {result.returncode}: {result.stderr}") + 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).""" From cefc56c76f7e0a7bc30497be72af30a9368f1aff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 06:14:13 +0000 Subject: [PATCH 12/13] Make pandas an optional dependency again - Removed pandas from requirements-core.txt - Added pandas>=1.4.0 to extras_require['dataframe'] and ['all'] - Updated README to reflect pandas, numpy, and matplotlib are all optional - Users can now install minimal version without any heavy dependencies - All 55 tests pass Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- README.md | 7 ++++--- requirements-core.txt | 1 - setup.py | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index da6c727..d7ea10b 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,15 @@ To install the latest release: `pip install prometheus-api-client` -To install with all optional dependencies (numpy, matplotlib): +To install with all optional dependencies (pandas, numpy, matplotlib): `pip install prometheus-api-client[all]` -**Note:** Starting from version 0.7.0, numpy and matplotlib are optional dependencies. Pandas is included as a core dependency. -The lazy import mechanism ensures that `PrometheusConnect` can be imported without loading numpy until `get_metric_aggregation()` is called, which reduces initial memory footprint. +**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 numpy support: `pip install prometheus-api-client[numpy]` - For plotting support: `pip install prometheus-api-client[plot]` diff --git a/requirements-core.txt b/requirements-core.txt index cc00aef..bbfd6bf 100644 --- a/requirements-core.txt +++ b/requirements-core.txt @@ -1,3 +1,2 @@ requests dateparser -pandas>=1.4.0 diff --git a/setup.py b/setup.py index 3a71d9e..4a6c1ab 100644 --- a/setup.py +++ b/setup.py @@ -37,9 +37,10 @@ def get_version(): 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"], - "all": ["numpy", "matplotlib"], + "all": ["pandas>=1.4.0", "numpy", "matplotlib"], }, packages=setuptools.find_packages(), package_data={"prometheus-api-client": ["py.typed"]}, From abc430e01701ca998edb5cad0b95f8090bc78dc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 01:32:10 +0000 Subject: [PATCH 13/13] Add use-case oriented extras and helpful error messages - Added 'analytics' extra for use-case oriented installation - Added try/except blocks with helpful error messages for missing dependencies: - NumPy: suggests installing [analytics] or [all] - Pandas: suggests installing [dataframe] or [all] - Updated README to highlight analytics extra instead of numpy - Error messages guide users to install correct extras - All 55 tests pass Co-authored-by: 4n4nd <22333506+4n4nd@users.noreply.github.com> --- README.md | 2 +- prometheus_api_client/metric.py | 10 +++++++++- prometheus_api_client/metric_range_df.py | 12 ++++++++++-- prometheus_api_client/metric_snapshot_df.py | 12 ++++++++++-- prometheus_api_client/prometheus_connect.py | 9 ++++++++- setup.py | 1 + 6 files changed, 39 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d7ea10b..bce2baa 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ If you only need `PrometheusConnect` without DataFrame support or plotting capab To install only specific extras: - For DataFrame support: `pip install prometheus-api-client[dataframe]` -- For numpy support: `pip install prometheus-api-client[numpy]` +- 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: 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 0149930..6f74939 100644 --- a/prometheus_api_client/prometheus_connect.py +++ b/prometheus_api_client/prometheus_connect.py @@ -568,7 +568,14 @@ def get_metric_aggregation( 'max': 6.009373 } """ - import numpy + 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") diff --git a/setup.py b/setup.py index 4a6c1ab..120d9ad 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ def get_version(): "dataframe": ["pandas>=1.4.0"], "numpy": ["numpy"], "plot": ["matplotlib"], + "analytics": ["numpy"], "all": ["pandas>=1.4.0", "numpy", "matplotlib"], }, packages=setuptools.find_packages(),