1#!/usr/bin/env python3
2"""
3
4"""
5
6# Imports:
7from __future__ import annotations
8
9# ##-- stdlib imports
10import datetime
11import enum
12import functools as ftz
13import importlib
14import itertools as itz
15import logging as logmod
16import pathlib as pl
17import re
18import time
19import weakref
20from uuid import UUID, uuid1
21
22# ##-- end stdlib imports
23
24# ##-- 3rd party imports
25from jgdv.structs.chainguard import ChainGuard
26from jgdv.structs.dkey import DKey
27from jgdv.structs.locator import Location
28
29# ##-- end 3rd party imports
30
31# ##-- 1st party imports
32import doot
33import doot.errors
34from .. import _interface as API # noqa: N812
35from .._interface import ArtifactStatus_e
36
37# ##-- end 1st party imports
38
39# ##-- types
40# isort: off
41# General
42import abc
43import collections.abc
44import typing
45import types
46from typing import cast, assert_type, assert_never
47from typing import Generic, NewType, Never
48from typing import no_type_check, final, override, overload
49# Protocols and Interfaces:
50from typing import Protocol, runtime_checkable
51if typing.TYPE_CHECKING:
52 from typing import Final, ClassVar, Any, Self
53 from typing import Literal, LiteralString
54 from typing import TypeGuard
55 from collections.abc import Iterable, Iterator, Callable, Generator
56 from collections.abc import Sequence, Mapping, MutableMapping, Hashable
57
58 from jgdv import Maybe, TimeDelta
59
60# isort: on
61# ##-- end types
62
63##-- logging
64logging = logmod.getLogger(__name__)
65##-- end logging
66
[docs]
67class TaskArtifact(Location):
68 """
69 A Location, but specialized to represent artifacts/files
70 An concrete or abstract artifact a task can produce or consume.
71
72 Tasks can depend on both concrete and abstract:
73 depends_on=['file:>/a/file.txt', 'file:>*.txt', 'file:>?.txt']
74 and can be a requirement for concrete or *solo* abstract artifacts:
75 required_for=['file:>a/file.txt', 'file:>?.txt']
76
77 """
78 __slots__ = ("priority",)
79 priority : int
80
81 def __init__(self, *args:Any, **kwargs:Any) -> None:
82 super().__init__(*args, **kwargs)
83 self.priority = API.DEFAULT_PRIORITY
84
85 def __bool__(self):
86 return self.exists()
87
[docs]
88 @property
89 def parent(self) -> pl.Path:
90 return self.path.parent
91
[docs]
92 def is_stale(self, *, delta:Maybe[TimeDelta]=None) -> bool:
93 """ whether the artifact itself is stale
94 delta defaults to 1 day
95 """
96 match delta:
97 case None:
98 return self < datetime.timedelta(days=1)
99 case datetime.timedelta():
100 return self < delta
101 case _:
102 raise NotImplementedError()
103
[docs]
104 def reify(self, other:pl.Path|Location) -> Maybe[TaskArtifact]: # noqa: PLR0912, PLR0915
105 """
106 Apply a more concrete path onto this location
107 """
108 if self.is_concrete():
109 raise NotImplementedError("Can't reify an already concrete location", self, other)
110
111 match other:
112 case pl.Path() | str():
113 other = Location(other)
114 case _:
115 pass
116
117 result = []
118 add_rest = False
119 # Compare path
120 for x,y in itz.zip_longest(self.body_parent, other.body_parent):
121 if add_rest:
122 result.append(y or x)
123 continue
124 match x, y:
125 case _, None:
126 result.append(x)
127 case None, _:
128 result.append(y)
129 case _, _ if x == y:
130 result.append(x)
131 case str() as x, _ if x == self.Wild.rec_glob:
132 add_rest = True
133 result.append(y)
134 case str() as x, str() if x in self.Wild:
135 result.append(y)
136 case str(), str() as y if y in self.Wild:
137 result.append(x)
138 case str(), str():
139 return None
140
141 logging.debug("%s and %s match on path", self, other)
142 # Compare the stem/ext
143 stem, ext = "", ""
144 match self.stem, other.stem:
145 case None, None:
146 pass
147 case None, str() as y:
148 stem = y
149 case str() as x, None:
150 stem = x
151 case str() as x, str() as y if x == y:
152 stem = x
153 case (xa, ya), (xb, yb) if xa == xb and ya == yb:
154 stem = ya
155 case (xa, ya), str() as xb:
156 stem = xb
157 case _, _:
158 return None
159
160 logging.debug("%s and %s match on stem", self, other)
161 match self.ext(), other.ext():
162 case None, None:
163 pass
164 case (xa, ya), (xb, yb) if xa == xb and ya == yb:
165 ext = ya
166 case (x, y), str() as yb:
167 ext = yb
168 case (_, x), None:
169 ext = x
170 case None, (_, y):
171 ext = y
172 case str() as x, None:
173 ext = x
174 case None, str() as y:
175 ext = y
176 case str() as x, str() as y if x == y:
177 ext = x
178 case _, _:
179 return None
180
181 logging.debug("%s and %s match", self, other)
182 result.append(f"{stem}{ext}")
183
184 return self.__class__("/".join(result))
185
[docs]
186 def exists(self) -> bool:
187 as_path = self.path
188 expanded = doot.locs[as_path] # type: ignore[attr-defined]
189 return expanded.exists()
190
[docs]
191 @override
192 def is_concrete(self) -> bool:
193 if self.Marks.abstract in self:
194 return False
195 try:
196 _ = doot.locs.expand(self)
197 except KeyError:
198 return False
199 else:
200 return True
201
202
[docs]
203 def get_status(self) -> ArtifactStatus_e:
204 """ Get the status of the artifact,
205 To start, either declared, or exists.
206 TODO: add a stale check
207 TODO: add a to-clean check
208 """
209 if self.exists():
210 return ArtifactStatus_e.EXISTS
211
212
213 return ArtifactStatus_e.DECLARED