Passed
Push — main ( a4501a...f8490e )
by Douglas
03:37
created

MandosResources.a_file()   A

Complexity

Conditions 2

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nop 3
dl 0
loc 18
rs 9.9
c 0
b 0
f 0
1
import logging
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import typing
3
from datetime import datetime, timedelta
4
from pathlib import Path
5
from typing import MutableMapping, Optional, Union
6
7
import orjson
0 ignored issues
show
introduced by
Unable to import 'orjson'
Loading history...
8
9
from pocketutils.core import PathLike
10
from pocketutils.core.chars import Chars
11
from pocketutils.core.dot_dict import NestedDotDict
12
from pocketutils.core.exceptions import (
13
    FileDoesNotExistError,
14
    MissingResourceError,
15
    PathExistsError,
16
    DirDoesNotExistError,
17
)
18
from pocketutils.tools.common_tools import CommonTools
19
from pocketutils.tools.unit_tools import UnitTools
20
from pocketutils.tools.filesys_tools import FilesysTools
21
22
23
_logger = logging.getLogger("pocketutils")
24
25
26
class Resources:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
27
    def __init__(self, path: PathLike, *, logger=_logger):
28
        self._dir = Path(path)
29
        self._logger = logger
30
31
    def contains(self, *nodes: PathLike, suffix: Optional[str] = None) -> bool:
32
        """Returns whether a resource file (or dir) exists."""
33
        return self.path(*nodes, suffix=suffix).exists()
34
35
    def path(self, *nodes: PathLike, suffix: Optional[str] = None, exists: bool = False) -> Path:
36
        """
37
        Gets a path of a test resource file under ``resources/``.
38
39
        Raises:
40
            MissingResourceError: If the path is not found
41
        """
42
        path = Path(self._dir, "resources", *nodes)
43
        path = path.with_suffix(path.suffix if suffix is None else suffix)
44
        if exists and not path.exists():
45
            raise MissingResourceError(f"Resource {path} missing")
46
        return path
47
48
    def file(self, *nodes: PathLike, suffix: Optional[str] = None) -> Path:
49
        """
50
        Gets a path of a test resource file under ``resources/``.
51
52
        Raises:
53
            MissingResourceError: If the path is not found
54
            PathExistsError: If the path is not a file or symlink to a file,
55
                             or is not readable
56
        """
57
        path = self.path(*nodes, suffix=suffix)
58
        info = FilesysTools.get_info(path)
59
        if not info.is_file:
60
            raise PathExistsError(f"Resource {path} is not a file!")
61
        if not info.is_readable_file:
62
            raise FileDoesNotExistError(f"Resource {path} is not readable")
63
        return path
64
65
    def dir(self, *nodes: PathLike) -> Path:
66
        """
67
        Gets a path of a test resource file under ``resources/``.
68
69
        Raises:
70
            MissingResourceError: If the path is not found and ``not missing_ok``
71
            PathExistsError: If the path is not a dir, symlink to a dir, or mount,
72
                             or lacks 'R' or 'X' permissions
73
        """
74
        path = self.path(*nodes)
75
        info = FilesysTools.get_info(path)
76
        if not info.exists:
77
            raise DirDoesNotExistError(f"Resource {path} does not exist")
78
        if not info.is_dir:
79
            raise PathExistsError(f"Resource {path} is not a directory")
80
        if info.is_readable_dir:
81
            raise FileDoesNotExistError(f"Resource {path} is not readable")
82
        return path
83
84
    def a_file(self, *nodes: PathLike, suffixes: Optional[typing.Set[str]] = None) -> Path:
85
        """
86
        Gets a path of a test resource file under ``resources/``, ignoring suffix.
87
88
        Args:
89
            nodes: Path nodes under ``resources/``
90
            suffixes: Set of acceptable suffixes; if None, all are accepted
91
        """
92
        path = Path(self._dir, "resources", *nodes)
93
        options = [
94
            p
95
            for p in path.parent.glob(path.stem + "*")
96
            if p.is_file() and (suffixes is None or p.suffix in suffixes)
97
        ]
98
        try:
99
            return CommonTools.only(options)
100
        except LookupError:
101
            raise MissingResourceError(f"Resource {path} missing") from None
102
103
    def json(self, *nodes: PathLike, suffix: Optional[str] = None) -> NestedDotDict:
104
        """Reads a JSON file under ``resources/``."""
105
        path = self.path(*nodes, suffix=suffix)
106
        data = orjson.loads(Path(path).read_text(encoding="utf8", errors="strict"))
107
        return NestedDotDict(data)
108
109
    def json_dict(self, *nodes: PathLike, suffix: Optional[str] = None) -> MutableMapping:
110
        """Reads a JSON file under ``resources/``."""
111
        path = self.path(*nodes, suffix=suffix)
112
        data = orjson.loads(Path(path).read_text(encoding="utf8", errors="strict"))
113
        return data
114
115
    def check_expired(
116
        self, path: PathLike, max_sec: Union[timedelta, float], *, what: Optional[str] = None
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
117
    ) -> bool:
118
        """
119
        Warns and returns True if ``path`` mod date is more than ``max_sec`` in the past.
120
121
        Args:
122
            path: A specific path to check
123
            max_sec: Max seconds, or a timedelta
124
            what: Substitute the path with this string in logging
125
        """
126
        path = Path(path)
127
        what = str(path) if what is None else what
128
        limit = max_sec if isinstance(max_sec, timedelta) else timedelta(seconds=max_sec)
129
        now = datetime.now().astimezone()
130
        then = FilesysTools.get_info(path).mod_or_create_dt
131
        delta = now - then
132
        if delta > limit:
133
            delta_str = UnitTools.delta_time_to_str(delta, space=Chars.narrownbsp)
134
            then_str = UnitTools.approx_time_wrt(now, then)
135
            self._logger.warning(f"{what} may be {delta_str} out of date. [{then_str}]")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
136
            return True
137
        return False
138
139
140
__all__ = ["Resources"]
141