Repository: OCR-D/core
Pull request #1176 (2024-01-26 14:47:36)

replace sparklines library with a simple replacement
The [sparklines](https://github.com/deeplook/sparklines) library we used to visualize memory consumption as block characters is GPLv3, so I made a little replacement that does the same, mostly.


Patch:
diff --git a/requirements.txt b/requirements.txt
index 9a9b2172..2a7a6f2f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -27,7 +27,6 @@ pyyaml
 requests < 2.30
 requests_unixsocket
 shapely
-sparklines >= 0.4.2
 # tensorflow versions might require different versions
 uvicorn
 uvicorn>=0.17.6
diff --git a/src/ocrd/processor/helpers.py b/src/ocrd/processor/helpers.py
index 209fa5f6..f5b60106 100644
--- a/src/ocrd/processor/helpers.py
+++ b/src/ocrd/processor/helpers.py
@@ -11,7 +11,7 @@ from typing import List
 
 from click import wrap_text
 from ocrd.workspace import Workspace
-from ocrd_utils import freeze_args, getLogger, config, setOverrideLogLevel, getLevelName
+from ocrd_utils import freeze_args, getLogger, config, setOverrideLogLevel, getLevelName, sparkline
 
 
 __all__ = [
@@ -106,7 +106,6 @@ def run_processor(
     if any(x in config.OCRD_PROFILE for x in ['RSS', 'PSS']):
         backend = 'psutil_pss' if 'PSS' in config.OCRD_PROFILE else 'psutil'
         from memory_profiler import memory_usage
-        from sparklines import sparklines
         try:
             mem_usage = memory_usage(proc=processor.process,
                                      # only run process once
@@ -123,7 +122,7 @@ def run_processor(
             chdir(old_cwd)
         mem_usage_values = [mem for mem, _ in mem_usage]
         mem_output = 'memory consumption: '
-        mem_output += ''.join(sparklines(mem_usage_values))
+        mem_output += sparkline(mem_usage_values)
         mem_output += ' max: %.2f MiB min: %.2f MiB' % (max(mem_usage_values), min(mem_usage_values))
         logProfile.info(mem_output)
     else:
diff --git a/src/ocrd_utils/__init__.py b/src/ocrd_utils/__init__.py
index 90cd5547..e566ce5f 100644
--- a/src/ocrd_utils/__init__.py
+++ b/src/ocrd_utils/__init__.py
@@ -198,6 +198,7 @@ from .str import (
     partition_list,
     parse_json_string_or_file,
     parse_json_string_with_comments,
+    sparkline,
     remove_non_path_from_url,
     safe_filename)
 
diff --git a/src/ocrd_utils/constants.py b/src/ocrd_utils/constants.py
index 0b9f0ae0..dc255bc1 100644
--- a/src/ocrd_utils/constants.py
+++ b/src/ocrd_utils/constants.py
@@ -110,3 +110,24 @@ LOG_TIMEFMT = r'%H:%M:%S'
 RESOURCE_LOCATIONS = ['data', 'cwd', 'system', 'module']
 
 DEFAULT_METS_BASENAME = 'mets.xml'
+
+
+#    2581 ▁ LOWER ONE EIGHTH BLOCK
+#    2582 ▂ LOWER ONE QUARTER BLOCK
+#    2583 ▃ LOWER THREE EIGHTHS BLOCK
+#    2584 ▄ LOWER HALF BLOCK
+#    2585 ▅ LOWER FIVE EIGHTHS BLOCK
+#    2586 ▆ LOWER THREE QUARTERS BLOCK
+#    2587 ▇ LOWER SEVEN EIGHTHS BLOCK
+#    2588 █ FULL BLOCK
+SPARKLINE_CHARS = [
+    ' ',
+    '\u2581',
+    '\u2582',
+    '\u2583',
+    '\u2584',
+    '\u2585',
+    '\u2586',
+    '\u2587',
+    '\u2588',
+]
diff --git a/src/ocrd_utils/str.py b/src/ocrd_utils/str.py
index f5b9242d..406fcc97 100644
--- a/src/ocrd_utils/str.py
+++ b/src/ocrd_utils/str.py
@@ -4,7 +4,8 @@ Utility functions for strings, paths and URL.
 
 import re
 import json
-from .constants import REGEX_FILE_ID
+from typing import List, Union
+from .constants import REGEX_FILE_ID, SPARKLINE_CHARS
 from .deprecate import deprecation_warning
 from warnings import warn
 from numpy import array_split
@@ -235,3 +236,18 @@ def partition_list(lst, chunks, chunk_index=None):
     if chunk_index is not None:
         return [ret[chunk_index]]
     return ret
+
+def sparkline(values : List[int]) -> str:
+    """
+    Render a list of points with block characters
+    """
+    if any(x is None or not isinstance(x, (int, float)) or x < 0 for x in values):
+        # return an empty string on non-positive-int values, better not to
+        # output a sparkline than to cancel execution due to problematic input
+        return ''
+    max_value = max(values)
+    max_mapping = len(SPARKLINE_CHARS) - 1
+    # normalize to 0..1 and convert to index in SPARKLINE_CHARS
+    mapped = [int(x / max_value * max_mapping) for x in values]
+    return ''.join(SPARKLINE_CHARS[x] for x in mapped)
+


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index d2093c46..8fe3fd37 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -21,6 +21,7 @@ from ocrd_utils import (
     is_string,
     membername,
     generate_range,
+    sparkline,
 
     nth_url_segment,
     remove_non_path_from_url,
@@ -325,6 +326,12 @@ def test_partition_list():
     assert partition_list(lst_13, chunks=4) == [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10], [11, 12, 13]]
     assert partition_list(lst_13, chunks=4, chunk_index=1) == [[5, 6, 7]]
 
+def test_sparkline():
+    assert sparkline([5, 2, 3]) == '█▃▄'
+    assert sparkline([1000, 1, 2222]) == '▃ █'
+    assert sparkline([8, 7, 6, 5, 4, 3, 2, 1, 0]) == '█▇▆▅▄▃▂▁ '
+    assert sparkline([-1, None, 'forty-two']) == ''
+
 
 if __name__ == '__main__':
     main(__file__)