heatmap_cli 0.25.3__tar.gz → 0.25.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {heatmap_cli-0.25.3/src/heatmap_cli.egg-info → heatmap_cli-0.25.4}/PKG-INFO +1 -1
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/pyproject.toml +1 -1
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli/__init__.py +1 -2
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli/cli.py +70 -10
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli/heatmap.py +15 -8
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4/src/heatmap_cli.egg-info}/PKG-INFO +1 -1
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_demo_option.py +22 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_start_date_option.py +22 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_title_option.py +14 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/LICENSE.md +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/README.md +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/setup.cfg +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli/__main__.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/SOURCES.txt +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/dependency_links.txt +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/entry_points.txt +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/requires.txt +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/src/heatmap_cli.egg-info/top_level.txt +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_animate_by_week.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_animate_no_frames.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_animate_open.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_annotate_flag.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_annotation_truncation.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_cbar_flag.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_cmap_option.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_cmax_option.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_cmin_option.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_configure_cbar.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_debug_flag.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_end_date_option.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_env_flag.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_direct.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_errors.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_open.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_utils.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_heatmap_wrapper.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_help_flag.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_iso_53_week_year.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_no_annotate.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_open_heatmap.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_output_dir_option.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_purge_flag.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_quiet_flag.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_refresh_output_dir.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_setup_logging.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_verbose_flag.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_version_flag.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_week_option.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_year_option.py +0 -0
- {heatmap_cli-0.25.3 → heatmap_cli-0.25.4}/tests/test_yes_flag.py +0 -0
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
"Programming Language :: Python",
|
|
26
26
|
"Topic :: Scientific/Engineering :: Visualization",
|
|
27
27
|
]
|
|
28
|
-
version = "0.25.
|
|
28
|
+
version = "0.25.4"
|
|
29
29
|
keywords = ["heatmap", "cli", "visualization", "calendar", "data-viz"]
|
|
30
30
|
dependencies = [
|
|
31
31
|
"matplotlib",
|
|
@@ -31,7 +31,6 @@ import matplotlib as mpl
|
|
|
31
31
|
import matplotlib.pyplot as plt
|
|
32
32
|
import pandas as pd
|
|
33
33
|
import seaborn as sns
|
|
34
|
-
from PIL import Image
|
|
35
34
|
|
|
36
35
|
from heatmap_cli.heatmap import _get_output_dir
|
|
37
36
|
from heatmap_cli.heatmap import run as generate_heatmaps
|
|
@@ -194,7 +193,7 @@ class EnvironmentAction(argparse.Action):
|
|
|
194
193
|
for name, version in (
|
|
195
194
|
("matplotlib", mpl.__version__),
|
|
196
195
|
("pandas", pd.__version__),
|
|
197
|
-
("pillow",
|
|
196
|
+
("pillow", metadata.version("pillow")),
|
|
198
197
|
("seaborn", sns.__version__),
|
|
199
198
|
)
|
|
200
199
|
)
|
|
@@ -43,6 +43,7 @@ from heatmap_cli import (
|
|
|
43
43
|
setup_logging,
|
|
44
44
|
)
|
|
45
45
|
from heatmap_cli.heatmap import (
|
|
46
|
+
_sanitize_string_for_filename,
|
|
46
47
|
_weeks_in_iso_year,
|
|
47
48
|
)
|
|
48
49
|
from heatmap_cli.heatmap import (
|
|
@@ -208,8 +209,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
208
209
|
type=_valid_date,
|
|
209
210
|
default=None,
|
|
210
211
|
help=(
|
|
211
|
-
"filter from the date of the year from the CSV file "
|
|
212
|
-
"(default: %(default)s)"
|
|
212
|
+
"filter from the date of the year from the CSV file and "
|
|
213
|
+
"this will overwrite -y and -w option (default: %(default)s)"
|
|
213
214
|
),
|
|
214
215
|
metavar="START_DATE",
|
|
215
216
|
)
|
|
@@ -424,6 +425,49 @@ def _generate_cmap_help() -> str:
|
|
|
424
425
|
return cmap_help + cmap_default + "\n" + cmap_choices.rstrip()
|
|
425
426
|
|
|
426
427
|
|
|
428
|
+
def _resolve_date_filter_overrides(
|
|
429
|
+
parsed_args: argparse.Namespace,
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Apply --start-date/--end-date overrides to the year and week filters.
|
|
432
|
+
|
|
433
|
+
A --start-date/--end-date pair defines a self-contained calendar date
|
|
434
|
+
range that overrides the -y/--year and -w/--week cutoff, avoiding a
|
|
435
|
+
double filter between the ISO-week year column and the raw date column.
|
|
436
|
+
When only one endpoint is given it derives a partial-year view; when
|
|
437
|
+
both are given --end-date wins for the year/week since it caps the range.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
parsed_args (argparse.Namespace): The newly parsed arguments.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
None
|
|
444
|
+
"""
|
|
445
|
+
# Capture the user-supplied filters before --end-date/--start-date clobber
|
|
446
|
+
# them, so the overriding of -y/-w is reported instead of happening
|
|
447
|
+
# silently.
|
|
448
|
+
explicit_year = parsed_args.year
|
|
449
|
+
explicit_week = parsed_args.week
|
|
450
|
+
|
|
451
|
+
if parsed_args.end_date:
|
|
452
|
+
(year, week, _day) = parsed_args.end_date.isocalendar()
|
|
453
|
+
parsed_args.year = [year]
|
|
454
|
+
parsed_args.week = week
|
|
455
|
+
elif parsed_args.start_date:
|
|
456
|
+
if explicit_year is not None:
|
|
457
|
+
logger.warning(
|
|
458
|
+
"argument --start-date: --year is ignored when "
|
|
459
|
+
"--start-date is used.",
|
|
460
|
+
)
|
|
461
|
+
if explicit_week is not None:
|
|
462
|
+
logger.warning(
|
|
463
|
+
"argument --start-date: --week is ignored when "
|
|
464
|
+
"--start-date is used.",
|
|
465
|
+
)
|
|
466
|
+
(year, _week, _day) = parsed_args.start_date.isocalendar()
|
|
467
|
+
parsed_args.year = [year]
|
|
468
|
+
parsed_args.week = _weeks_in_iso_year(year)
|
|
469
|
+
|
|
470
|
+
|
|
427
471
|
def _process_args(
|
|
428
472
|
parser: argparse.ArgumentParser,
|
|
429
473
|
parsed_args: argparse.Namespace,
|
|
@@ -445,14 +489,7 @@ def _process_args(
|
|
|
445
489
|
# double filter between the ISO-week year column and the raw date column.
|
|
446
490
|
# When only one endpoint is given it derives a partial-year view; when
|
|
447
491
|
# both are given --end-date wins for the year/week since it caps the range.
|
|
448
|
-
|
|
449
|
-
(year, week, _day) = parsed_args.end_date.isocalendar()
|
|
450
|
-
parsed_args.year = [year]
|
|
451
|
-
parsed_args.week = week
|
|
452
|
-
elif parsed_args.start_date:
|
|
453
|
-
(year, _week, _day) = parsed_args.start_date.isocalendar()
|
|
454
|
-
parsed_args.year = [year]
|
|
455
|
-
parsed_args.week = _weeks_in_iso_year(year)
|
|
492
|
+
_resolve_date_filter_overrides(parsed_args)
|
|
456
493
|
|
|
457
494
|
if (
|
|
458
495
|
parsed_args.start_date
|
|
@@ -479,6 +516,10 @@ def _process_args(
|
|
|
479
516
|
if parsed_args.cmap is None:
|
|
480
517
|
parsed_args.cmap = [DEFAULT_CMAP]
|
|
481
518
|
|
|
519
|
+
# Reject titles that sanitize to an empty string, which would otherwise
|
|
520
|
+
# yield a degenerate filename like 001__<cmap>.png.
|
|
521
|
+
_validate_title(parser, parsed_args.title)
|
|
522
|
+
|
|
482
523
|
# A GIF output is only ever produced by the animation path, so asking for
|
|
483
524
|
# gif implies --animate-by-week; otherwise fig.savefig(format="gif") would
|
|
484
525
|
# emit a single-frame still instead of an animation.
|
|
@@ -488,6 +529,23 @@ def _process_args(
|
|
|
488
529
|
return parsed_args
|
|
489
530
|
|
|
490
531
|
|
|
532
|
+
def _validate_title(
|
|
533
|
+
parser: argparse.ArgumentParser,
|
|
534
|
+
title: str | None,
|
|
535
|
+
) -> None:
|
|
536
|
+
"""Reject titles that would produce a degenerate filename.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
parser (argparse.ArgumentParser): The argument parser instance.
|
|
540
|
+
title (str | None): The title value from the command line.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
None
|
|
544
|
+
"""
|
|
545
|
+
if title is not None and not _sanitize_string_for_filename(title):
|
|
546
|
+
parser.error("argument --title: cannot be empty or symbolic only.")
|
|
547
|
+
|
|
548
|
+
|
|
491
549
|
def main(args: Sequence[str] | None = None) -> None:
|
|
492
550
|
"""Run the main program flow.
|
|
493
551
|
|
|
@@ -510,6 +568,8 @@ def main(args: Sequence[str] | None = None) -> None:
|
|
|
510
568
|
|
|
511
569
|
processed_args = _process_args(parser, parsed_args)
|
|
512
570
|
setup_logging(processed_args)
|
|
571
|
+
if processed_args.seed is not None and not processed_args.demo:
|
|
572
|
+
logger.warning("--seed is ignored unless --demo is used")
|
|
513
573
|
if processed_args.demo:
|
|
514
574
|
generate_demo(processed_args)
|
|
515
575
|
return
|
|
@@ -300,14 +300,21 @@ def _generate_heatmap_wrapper(
|
|
|
300
300
|
cmap: str,
|
|
301
301
|
config: argparse.Namespace,
|
|
302
302
|
dataframe: pd.DataFrame,
|
|
303
|
-
) ->
|
|
304
|
-
"""Wrapper for _generate_heatmap to handle args in Pool.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
) -> Path | None:
|
|
304
|
+
"""Wrapper for _generate_heatmap to handle args in Pool.
|
|
305
|
+
|
|
306
|
+
Workers always save to disk (``return_image=False``), so the result is a
|
|
307
|
+
file Path, never an in-memory image.
|
|
308
|
+
"""
|
|
309
|
+
return cast(
|
|
310
|
+
"Path | None",
|
|
311
|
+
_generate_heatmap(
|
|
312
|
+
seq,
|
|
313
|
+
cmap,
|
|
314
|
+
config,
|
|
315
|
+
dataframe,
|
|
316
|
+
return_image=False,
|
|
317
|
+
),
|
|
311
318
|
)
|
|
312
319
|
|
|
313
320
|
|
|
@@ -109,6 +109,28 @@ def test_demo_with_purge_survives(cli_runner):
|
|
|
109
109
|
assert "Input file not found" not in ret.stderr
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
def test_seed_without_demo_warns(cli_runner, csv_file):
|
|
113
|
+
csv = csv_file("sample.csv")
|
|
114
|
+
ret = cli_runner(
|
|
115
|
+
csv,
|
|
116
|
+
"-y",
|
|
117
|
+
"2024",
|
|
118
|
+
"-w",
|
|
119
|
+
"52",
|
|
120
|
+
"--seed",
|
|
121
|
+
"42",
|
|
122
|
+
expect_error=False,
|
|
123
|
+
)
|
|
124
|
+
assert ret.returncode == 0
|
|
125
|
+
assert "--seed is ignored unless --demo is used" in ret.stderr
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_seed_with_demo_no_warning(cli_runner):
|
|
129
|
+
ret = cli_runner("--demo", 1, "--seed", "42", expect_error=False)
|
|
130
|
+
assert ret.returncode == 0
|
|
131
|
+
assert "--seed is ignored unless --demo is used" not in ret.stderr
|
|
132
|
+
|
|
133
|
+
|
|
112
134
|
def test_demo_with_tilde_output_dir(cli_runner, monkeypatch, tmpdir):
|
|
113
135
|
"""--demo must expand ~ in --output-dir like the rest of the pipeline."""
|
|
114
136
|
home = Path(tmpdir, "home")
|
|
@@ -76,3 +76,25 @@ def test_start_date_overrides_year_cutoff(cli_runner, csv_file):
|
|
|
76
76
|
csv = csv_file("sample.csv")
|
|
77
77
|
ret = cli_runner(csv, "-d", "-s", "2023-06-01", "--year", "2024")
|
|
78
78
|
assert "year=[2023]" in ret.stderr
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_start_date_warns_when_year_overridden(cli_runner, csv_file):
|
|
82
|
+
"""Passing -y with --start-date must warn that -y is ignored."""
|
|
83
|
+
csv = csv_file("sample.csv")
|
|
84
|
+
ret = cli_runner(csv, "-s", "2023-06-01", "--year", "2024")
|
|
85
|
+
assert "argument --start-date: --year is ignored when" in ret.stderr
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("option", ["-w", "--week"])
|
|
89
|
+
def test_start_date_warns_when_week_overridden(cli_runner, csv_file, option):
|
|
90
|
+
"""Passing -w with --start-date must warn that -w is ignored."""
|
|
91
|
+
csv = csv_file("sample.csv")
|
|
92
|
+
ret = cli_runner(csv, "-s", "2023-06-01", option, "20")
|
|
93
|
+
assert "argument --start-date: --week is ignored when" in ret.stderr
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_start_date_alone_no_override_warning(cli_runner, csv_file):
|
|
97
|
+
"""--start-date used alone must not emit an override warning."""
|
|
98
|
+
csv = csv_file("sample.csv")
|
|
99
|
+
ret = cli_runner(csv, "-s", "2023-06-01")
|
|
100
|
+
assert "is ignored when --start-date" not in ret.stderr
|
|
@@ -25,6 +25,20 @@ def test_debug_logs(cli_runner, csv_file, option):
|
|
|
25
25
|
assert "my title is foobar" in ret.stderr
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
@pytest.mark.parametrize("option", ["-t", "--title"])
|
|
29
|
+
def test_empty_title_rejected(cli_runner, csv_file, option):
|
|
30
|
+
csv = csv_file("sample.csv")
|
|
31
|
+
ret = cli_runner(csv, "-y", "2024", option, "")
|
|
32
|
+
assert "argument --title: cannot be empty or symbolic only." in ret.stderr
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize("option", ["-t", "--title"])
|
|
36
|
+
def test_symbols_only_title_rejected(cli_runner, csv_file, option):
|
|
37
|
+
csv = csv_file("sample.csv")
|
|
38
|
+
ret = cli_runner(csv, "-y", "2024", option, "!!!")
|
|
39
|
+
assert "argument --title: cannot be empty or symbolic only." in ret.stderr
|
|
40
|
+
|
|
41
|
+
|
|
28
42
|
def test_default_title(cli_runner, csv_file):
|
|
29
43
|
csv = csv_file("sample.csv")
|
|
30
44
|
ret = cli_runner(csv, "-y", "2024", "-d", "-w", "42")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|