Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 numpy support: `pip install prometheus-api-client[numpy]`
- For plotting support: `pip install prometheus-api-client[plot]`
Comment on lines +21 to +24
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we consider making these installations use-case oriented instead of package oriented, e.g. something like [analytics] instead of [numpy]? It's not obvious to me (let alone end users) what installing with [numpy] would enable me to do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added use-case oriented [analytics] extra in commit abc430e. Updated README to highlight this instead of [numpy]. The [numpy] extra still exists for backward compatibility.


To install directly from this branch:

`pip install https://github.com/4n4nd/prometheus-api-client-python/zipball/master`
Expand Down
2 changes: 1 addition & 1 deletion prometheus_api_client/__init__.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
3 changes: 2 additions & 1 deletion prometheus_api_client/prometheus_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
import json
import logging
import numpy
from datetime import datetime, timedelta
import requests
from requests.adapters import HTTPAdapter
Expand Down Expand Up @@ -569,6 +568,8 @@ def get_metric_aggregation(
'max': 6.009373
}
"""
import numpy
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put some try/except here? If a user has installed the minimal version and ties calling this function, they will get a generic import error. IMO we should explicitly mention that they’ll need to install the [numpy] or [full] version.

I believe this suggestion applies for Metric, MetricPlot, and other classes as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added try/except blocks with helpful error messages in commit abc430e:

  • NumPy import in get_metric_aggregation() suggests installing [analytics] or [all]
  • Pandas imports in Metric, MetricRangeDataFrame, and MetricSnapshotDataFrame suggest installing [dataframe] or [all]

All error messages clearly guide users to the correct installation command.


if not isinstance(operations, list):
raise TypeError("Operations can be only of type list")
if len(operations) == 0:
Expand Down
2 changes: 2 additions & 0 deletions requirements-core.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests
dateparser
10 changes: 8 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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"],
Expand Down
100 changes: 100 additions & 0 deletions tests/test_lazy_imports.py
Original file line number Diff line number Diff line change
@@ -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 = """
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Consider using textwrap.dedent for multi-line text.

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 = """
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

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()
Loading