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