Coverage for puhdistusalue/cli.py: 61.00%
66 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 21:59:31 +00:00
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 21:59:31 +00:00
1#! /usr/bin/env python
2# -*- coding: utf-8 -*-
3"""Purge monotonically named files in folders keeping range endpoints.
5Implementation uses sha256 hashes for identity and assumes that
6the natural order relates to the notion of fresher or better.
7"""
8import datetime as dti
9import os
10import sys
11import typing
13from puristaa.puristaa import prefix_compression # type: ignore
15from puhdistusalue.puhdistusalue import read_folder, triage_hashes
17DEBUG = os.getenv('PURGE_RANGE_DEBUG')
20@typing.no_type_check
21def humanize_mass(total_less_bytes: int):
22 """DRY"""
23 if total_less_bytes >= 1e9:
24 return f'{round(total_less_bytes / 1024 / 1024 / 1024, 3) :.3f}', 'total gigabytes'
25 if total_less_bytes >= 1e6:
26 return f'{round(total_less_bytes / 1024 / 1024, 3) :.3f}', 'total megabytes'
27 if total_less_bytes >= 1e3:
28 return f'{round(total_less_bytes / 1024, 3) :.3f}', 'total kilobytes'
30 return f'{total_less_bytes :d}', 'total bytes'
33@typing.no_type_check
34def humanize_duration(duration_seconds: float):
35 """DRY"""
36 if duration_seconds >= 3600:
37 return f'{round(duration_seconds / 60 / 60, 3) :.3f}', 'hours'
38 if duration_seconds >= 60:
39 return f'{round(duration_seconds / 60, 3) :.3f}', 'minutes'
40 if duration_seconds >= 1:
41 return f'{round(duration_seconds, 3) :.3f}', 'seconds'
43 return f'{round(duration_seconds * 1e3, 3) :.3f}', 'millis'
46# pylint: disable=expression-not-assigned
47@typing.no_type_check
48def main(argv=None):
49 """Process the files separately per folder."""
50 start_time = dti.datetime.now(dti.UTC)
51 argv = sys.argv[1:] if argv is None else argv
52 verbose = bool('-v' in argv or '--verbose' in argv)
53 human = bool('-H' in argv or '--human' in argv)
54 folder_paths = [entry for entry in argv if entry.strip() and entry not in ('-H', '--human', '-v', '--verbose')]
55 total_removed, total_less_bytes = 0, 0
56 for a_path in folder_paths:
57 hash_map = {}
58 try:
59 hash_map = read_folder(a_path)
60 except FileNotFoundError as err:
61 print(f'WARNING: Skipping non-existing path ({a_path}) -> "{err}"')
62 if hash_map:
63 keep_these, remove_those = triage_hashes(hash_map)
64 for this in keep_these:
65 DEBUG and print(f'KEEP file {this}')
66 folder_removed, folder_less_bytes = 0, 0
67 for that in remove_those: 67 ↛ 68line 67 didn't jump to line 68, because the loop on line 67 never started
68 DEBUG and print(f'REMOVE file {that}')
69 target = os.path.join(a_path, that)
70 folder_less_bytes += os.path.getsize(target)
71 os.remove(target)
72 folder_removed += 1
74 if verbose: 74 ↛ 75line 74 didn't jump to line 75, because the condition on line 74 was never true
75 print(
76 f'removed {folder_removed} redundant objects or {folder_less_bytes}'
77 f' combined bytes from folder at {a_path}'
78 )
79 total_less_bytes += folder_less_bytes
80 total_removed += folder_removed
82 prefix, rel_paths = prefix_compression(folder_paths, policy=lambda x: x == '/')
83 if len(rel_paths) > 5: 83 ↛ 84line 83 didn't jump to line 84, because the condition on line 83 was never true
84 folders_disp = f"{prefix}[{', '.join(rel_paths[:3])}, ... {rel_paths[-1]}]"
85 else:
86 folders_disp = f'{folder_paths}' if folder_paths else '[<EMPTY>]'
88 duration_seconds = (dti.datetime.now(dti.UTC) - start_time).total_seconds()
89 if human: 89 ↛ 90line 89 didn't jump to line 90, because the condition on line 89 was never true
90 m_quantity, m_unit = humanize_mass(total_less_bytes)
91 d_quantity, d_unit = humanize_duration(duration_seconds)
92 else:
93 m_quantity, m_unit = f'{total_less_bytes :d}', 'total bytes'
94 d_quantity, d_unit = f'{round(duration_seconds, 3) :.3f}', 'seconds'
96 print(
97 f'removed {total_removed} total redundant objects or'
98 f' {m_quantity} {m_unit} from folders at {folders_disp}'
99 f' in {d_quantity} {d_unit}'
100 )