Source code for jgdv.structs.locator.location

  1#!/usr/bin/env python3
  2"""
  3
  4"""
  5# mypy: disable-error-code="attr-defined"
  6# ruff: noqa: ANN002, ANN003
  7# Imports:
  8from __future__ import annotations
  9
 10# ##-- stdlib imports
 11import datetime
 12import functools as ftz
 13import itertools as itz
 14import logging as logmod
 15import pathlib as pl
 16import re
 17import time
 18import types
 19import weakref
 20from uuid import UUID, uuid1
 21
 22# ##-- end stdlib imports
 23
 24# ##-- 3rd party imports
 25from pydantic import BaseModel, field_validator, model_validator
 26
 27# ##-- end 3rd party imports
 28
 29# ##-- 1st party imports
 30from jgdv import Proto
 31from jgdv.structs.dkey import DKey
 32from jgdv.mixins.path_manip import PathManip_m
 33from jgdv.structs.strang import Strang
 34
 35from jgdv.structs.strang import _interface as StrangAPI # noqa: N812
 36from jgdv.structs.strang._interface import Strang_p
 37# ##-- end 1st party imports
 38
 39from . import _interface as API # noqa: N812
 40from .processor import LocationProcessor
 41
 42# ##-- types
 43# isort: off
 44import abc
 45import collections.abc
 46from typing import TYPE_CHECKING, Generic, cast, assert_type, assert_never
 47# Protocols:
 48from typing import Protocol, runtime_checkable
 49# Typing Decorators:
 50from typing import no_type_check, final, override, overload
 51TimeDelta = datetime.timedelta
 52if TYPE_CHECKING:
 53   import enum
 54   from jgdv import Maybe
 55   from typing import Final
 56   from typing import ClassVar, Any, LiteralString
 57   from typing import Never, Self, Literal
 58   from typing import TypeGuard
 59   from collections.abc import Iterable, Iterator, Callable, Generator
 60   from collections.abc import Sequence, Mapping, MutableMapping, Hashable
 61
 62# isort: on
 63# ##-- end types
 64
 65##-- logging
 66logging = logmod.getLogger(__name__)
 67##-- end logging
 68
[docs] 69@Proto(API.Location_p) 70class Location(Strang): 71 """ A Location is an abstraction higher than a path. 72 73 ie: a path, with metadata. 74 75 Doesn't expand on its own, requires a JGDVLocator store 76 77 It is a Strang subclass, of the form "{meta}+::a/path/location". eg:: 78 79 file/clean::.temp/docs/blah.rst 80 81 TODO use annotations to require certain metaflags. 82 eg:: 83 84 ProtectedLoc = Location['protect'] 85 Cleanable = Location['clean'] 86 FileLoc = Location['file'] 87 88 TODO add an ExpandedLoc subclass that holds the expanded path, 89 and removes the need for much of PathManip_m? 90 91 TODO add a ShadowLoc subclass using annotations 92 eg:: 93 94 BackupTo = ShadowLoc[root='/vols/BackupSD'] 95 a_loc = BackupTo('file::a/b/c.mp3') 96 a_loc.path_pair() -> ('/vols/BackupSD/a/b/c.mp3', '~/a/b/c.mp3') 97 98 """ 99 __slots__ = () 100 101 _processor : ClassVar = LocationProcessor() 102 _sections : ClassVar = API.LocationSections 103 Marks : ClassVar = API.LocationMeta_e 104 Wild : ClassVar = API.WildCard_e 105 106 def __init__(self, *args, **kwargs) -> None: 107 super().__init__(*args, **kwargs) # type: ignore[misc] 108 109 @override 110 def __contains__(self, other:object) -> bool: 111 """ Whether a definite artifact is matched by self, an abstract artifact 112 113 | other ∈ self 114 | a/b/c.py ∈ a/b/*.py 115 | ________ ∈ a/*/c.py 116 | ________ ∈ a/b/c.* 117 | ________ ∈ a/*/c.* 118 | ________ ∈ **/c.py 119 | ________ ∈ a/b ie: self < other 120 """ 121 match other: 122 case self.Marks() as x: 123 return x in self.data.meta 124 case pl.Path() | API.Location_p() if self.Marks.abstract in self.data.meta: 125 return self.check_wildcards(other) 126 case API.Location_p(): 127 return self < other 128 case _: 129 return super().__contains__(other) # type: ignore[misc] 130 131 @override 132 def __lt__(self, other:TimeDelta|str|pl.Path|API.Location_p) -> bool: # type: ignore[override] 133 """ self < path|location 134 self < delta : self.modtime < (now - delta) 135 """ 136 match other: 137 case TimeDelta() if self.is_concrete(): 138 return False 139 case TimeDelta(): 140 raise NotImplementedError() 141 case _: 142 return super().__lt__(str(other)) # type: ignore[misc] 143
[docs] 144 @property 145 def path(self) -> pl.Path: 146 return pl.Path(self[1,:])
147
[docs] 148 @property 149 def body_parent(self) -> list[str|API.WildCard_e]: 150 if self.Marks.file in self: 151 return list(itz.islice(self.words(1), len(self.data.sec_words[1])-1)) 152 153 return list(self.words(1))
154
[docs] 155 @property 156 def stem(self) -> Maybe[str|tuple[API.WildCard_e, str]]: 157 """ Return the stem, or a tuple describing how it is a wildcard """ 158 result : Maybe[str|tuple[API.WildCard_e, str]] = None 159 if self.Marks.file not in self.data.meta: 160 return result 161 162 match self[1,-1]: 163 case str() as elem: 164 pass 165 case _: 166 return None 167 168 match elem.split(".")[0]: 169 case str() as elem if (wc:=self.Wild.glob) in elem: 170 result = (wc, elem) 171 case str() as elem if (wc:=self.Wild.select) in elem: 172 result = (wc, elem) 173 case str() as elem if (wc:=self.Wild.key) in elem: 174 result = (wc, elem) 175 case str() as elem: 176 result = elem 177 case _: 178 pass 179 180 return result
181
[docs] 182 @property 183 def keys(self) -> set[str]: 184 raise NotImplementedError()
185
[docs] 186 @property 187 def key(self) -> Maybe[str|API.Key_p]: 188 raise NotImplementedError()
189
[docs] 190 def ext(self, *, last:bool=False) -> Maybe[str|tuple[API.WildCard_e, str]]: # noqa: PLR0911 191 """ return the ext, or a tuple of how it is a wildcard. 192 returns nothing if theres no extension, 193 returns all suffixes if there are multiple, or just the last if last=True 194 """ 195 x : Any 196 if self.Marks.file not in self.data.meta: 197 return None 198 199 match self[1,-1]: 200 case str() as elem: 201 pass 202 case _: 203 return None 204 205 match elem.rfind(".") if last else elem.find("."): 206 case -1: 207 return None 208 case int() as x: 209 pass 210 case x: 211 raise TypeError(type(x)) 212 213 match elem[x:]: 214 case ".": 215 return None 216 case ext if (wc:=API.WildCard_e.glob) in ext: 217 return (wc, ext) 218 case ext if (wc:=API.WildCard_e.select) in ext: 219 return (wc, ext) 220 case ext: 221 return ext
222
[docs] 223 def is_concrete(self) -> bool: 224 return self.Marks.abstract not in self.data.meta
225
[docs] 226 def check_wildcards(self, other:pl.Path|API.Location_p) -> bool: # noqa: PLR0912 227 """ Return True if other is within self, accounting for wildcards """ 228 logging.debug("Checking %s < %s", self, other) 229 x : Any 230 other_ext : Any 231 other_parts : list[str] 232 if self.is_concrete(): 233 return self < other 234 235 match other: 236 case pl.Path() as x: 237 other_parts = list(x.parts) 238 other_ext = x.suffix 239 case API.Location_p() as x: 240 other_parts = [str(x) for x in other.body_parent] 241 other_ext = other.ext() 242 243 # Compare path up to the file 244 for x,y in zip(self.body_parent, other_parts, strict=False): 245 match x, y: 246 case str(), str() if x == y: 247 pass 248 case str(), str() if x not in self.Wild and y not in self.Wild: 249 return False 250 case x, _ if self.Wild(x) is self.Wild.rec_glob: 251 break 252 case x, str() if x in self.Wild: 253 pass 254 case str(), y if y in self.Wild: 255 pass 256 case str(), str(): 257 pass 258 259 if self.Marks.file not in self.data.meta: 260 return True 261 262 logging.debug("%s and %s match on path", self, other) 263 # Compare the stem/ext 264 match self.stem, other.stem: 265 case (xa, ya), (xb, yb) if xa == xb and ya == yb: 266 pass 267 case (xa, ya), str(): 268 pass 269 case str() as x, str() as y if x == y: 270 pass 271 case _, _: 272 return False 273 274 logging.debug("%s and %s match on stem", self, other) 275 match self.ext(), other_ext: 276 case None, None: 277 pass 278 case (xa, ya), (xb, yb) if xa == xb and ya == yb: 279 pass 280 case (x, y), _: 281 pass 282 case str() as x, str() as y if x == y: 283 pass 284 case _, _: 285 return False 286 287 logging.debug("%s and %s match", self, other) 288 return True