| 1 |  |  | import logging | 
                            
                    |  |  |  | 
                                                                                        
                                                                                     | 
            
                                                                                                            
                            
            
                                    
            
            
                | 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 | 
                            
                    |  |  |  | 
                                                                                        
                                                                                     | 
            
                                                                                                            
                            
            
                                    
            
            
                | 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: | 
                            
                    |  |  |  | 
                                                                                        
                                                                                     | 
            
                                                                                                            
                            
            
                                    
            
            
                | 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 | 
                            
                    |  |  |  | 
                                                                                        
                                                                                     | 
            
                                                                                                            
                            
            
                                    
            
            
                | 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}]") | 
                            
                    |  |  |  | 
                                                                                        
                                                                                     | 
            
                                                                                                            
                            
            
                                    
            
            
                | 136 |  |  |             return True | 
            
                                                                                                            
                            
            
                                    
            
            
                | 137 |  |  |         return False | 
            
                                                                                                            
                            
            
                                    
            
            
                | 138 |  |  |  | 
            
                                                                                                            
                            
            
                                    
            
            
                | 139 |  |  |  | 
            
                                                                                                            
                                                                
            
                                    
            
            
                | 140 |  |  | __all__ = ["Resources"] | 
            
                                                        
            
                                    
            
            
                | 141 |  |  |  |