Просмотр исходного кода

Merge remote-tracking branch 'upstream/master'

Mikael Koli 2 лет назад
Родитель
Сommit
2ca704e6c7

BIN
docs/imgs/table_with_styler.png


+ 69 - 0
docs/tutorials/embed/table.rst

@@ -77,3 +77,72 @@ if you wish to make your own table prettifying:
 
 The templates get parameter ``df`` which is the dataframe
 to be prettified.
+
+Using Pandas Styler
+-------------------
+
+You may also pass a Pandas style object to the body tables. 
+This feature depends on `css_inline <https://pypi.org/project/css-inline/>`_.
+
+You may install the requirements:
+
+.. code-block:: shell
+
+    pip install redmail[style]
+
+First we make a Pandas style object:
+
+.. code-block:: python
+
+    import pandas as pd
+
+    # Specify CSS style
+    styles = [
+        dict(
+            selector="th", 
+            props=[
+                ("font-weight", "bold"),
+                ("padding", "0.5em 0.5em"),
+                ("border-bottom", "1px solid black")
+            ]
+        ),
+        dict(
+            selector="tr:nth-child(even)", 
+            props=[("background-color", "#f5f5f5")]
+        ),
+        dict(
+            selector="tr:nth-child(odd)", 
+            props=[("background-color", "#FFFFFF")]
+        ),
+    ]
+
+    # Create a dataframe
+    df = pd.DataFrame({
+        'nums': [1,2,3],
+        'strings': ['yes', 'no', 'yes'],
+    })
+
+    # Set the style
+    style = (
+        df.style
+        .set_table_styles(styles)
+        .hide(axis="index")
+    )
+
+Then to send the email:
+
+.. code-block:: python
+
+    email.send(
+        subject='A prettified table',
+        receivers=['first.last@example.com'],
+        html="<h1>This is a table:</h1> {{ mytable }}",
+        body_tables={
+            'mytable': style, 
+        }
+    )
+
+The result looks like the following:
+
+.. image:: /imgs/table_with_styler.png
+    :align: left

+ 4 - 0
docs/versions.rst

@@ -4,6 +4,10 @@
 Version history
 ===============
 
+- ``0.6.0``
+
+    - Add: Support for Pandas styler (thanks ejbills!)
+
 - ``0.5.0``
 
     - Add: Option to set custom email headers.

+ 5 - 0
pyproject.toml

@@ -59,6 +59,7 @@ test = [
     'matplotlib',
     'Pillow',
     'openpyxl',
+    'css_inline',
 ]
 docs = [
     'sphinx >= 1.7.5',
@@ -68,6 +69,10 @@ docs = [
     'sphinx_book_theme',
 ]
 
+style = [
+    'css_inline',
+]
+
 [tool.coverage.run]
 source = ["redmail"]
 branch = false

+ 12 - 2
redmail/email/body.py

@@ -5,7 +5,6 @@ from pathlib import Path
 from typing import TYPE_CHECKING, Dict, Union, ByteString
 from pathlib import Path
 
-
 from redmail.utils import is_bytes
 from redmail.utils import import_from_string
 
@@ -16,7 +15,7 @@ from jinja2.environment import Template, Environment
 from markupsafe import Markup
 
 # We try to import matplotlib and PIL but if fails, they will be None
-from .utils import PIL, plt, pd
+from .utils import PIL, plt, pd, css_inline
 
 if TYPE_CHECKING:
     # For type hinting
@@ -62,7 +61,18 @@ class Body:
         if pd is None:
             raise ImportError("Missing package 'pandas'. Prettifying tables requires Pandas.")
         
+        from pandas.io.formats.style import Styler
+
         extra = {} if extra is None else extra
+
+        # Allow for pandas styler object, convert to inline CSS for email client rendering
+        # https://pandas.pydata.org/docs/reference/api/pandas.io.formats.style.Styler.html
+        if isinstance(tbl, Styler):
+            if css_inline is None:
+                raise ImportError("Missing package 'css_inline'. Prettifying tables with Pandas styler requires css_inline.")
+            inliner = css_inline.CSSInliner()
+            return inliner.inline(tbl.to_html())
+
         df = pd.DataFrame(tbl)
 
         tbl_html = self.table_template.render({"df": df, **extra})

+ 11 - 3
redmail/email/utils.py

@@ -1,6 +1,14 @@
 
+from typing import TYPE_CHECKING
 from redmail.utils import import_from_string
 
-plt = import_from_string("matplotlib.pyplot", if_missing="ignore")
-PIL = import_from_string("PIL", if_missing="ignore")
-pd = import_from_string("pandas", if_missing="ignore")
+if TYPE_CHECKING:
+    import matplotlib.pyplot as plt_lib
+    import PIL as PIL_lib
+    import pandas as pandas_lib
+    import css_inline as css_inline_lib
+
+plt: 'plt_lib' = import_from_string("matplotlib.pyplot", if_missing="ignore")
+PIL: 'PIL_lib' = import_from_string("PIL", if_missing="ignore")
+pd: 'pandas_lib' = import_from_string("pandas", if_missing="ignore")
+css_inline: 'css_inline_lib' = import_from_string("css_inline", if_missing="ignore")

+ 33 - 1
redmail/test/email/test_inline_media.py

@@ -240,6 +240,14 @@ def test_with_image_error():
             ), 
             id="Multiindex end with spanned"
         ),
+        pytest.param(
+            lambda: pd.DataFrame(
+                [[1, 2], [4, 5]],
+                columns=pd.MultiIndex.from_tuples([("col a", "child b", "subchild a"), ("col a", "child b", "subchild a")]),
+                index=pd.MultiIndex.from_tuples([("row a", "child b", "subchild a"), ("row a", "child b", "subchild a")]),
+            ).style.set_caption("A caption"), 
+            id="With styler"
+        ),
         pytest.param(
             lambda: pd.DataFrame(
                 [],
@@ -292,4 +300,28 @@ def test_embed_tables_pandas_missing():
                 body_tables={"my_table": [{"col1": 1, "col2": "a"}, {"col1": 2, "col2": "b"}]}
             )
     finally:
-        body.pd = pd_mdl
+        body.pd = pd_mdl
+
+def test_styled_tables_dependency_missing():
+    pd = pytest.importorskip("pandas")
+
+    sender = EmailSender(host=None, port=1234)
+
+    from redmail.email import body
+    mdl = body.css_inline # This may be already None if env does not have Pandas
+    try:
+        # src uses this to reference Pandas (if missing --> None)
+        body.css_inline = None
+        style = pd.DataFrame(
+            {"col1": ["a", "b", "c"], "col2": [1, 2, 3]}
+        ).style.set_caption("A caption")
+        with pytest.raises(ImportError):
+            msg = sender.get_message(
+                sender="me@gmail.com",
+                receivers="you@gmail.com",
+                subject="Some news",
+                html='The table {{my_table}}',
+                body_tables={"my_table": style}
+            )
+    finally:
+        body.css_inline = mdl

+ 2 - 1
requirements.txt

@@ -5,4 +5,5 @@ jinja2
 pandas
 matplotlib
 Pillow
-openpyxl
+openpyxl
+css_inline

+ 2 - 1
requirements/coverage.txt

@@ -9,4 +9,5 @@ jinja2
 pandas
 matplotlib
 Pillow
-openpyxl
+openpyxl
+css_inline