11import logging
22from collections .abc import Iterable
3+ from itertools import chain
34from functools import cached_property
45
56import fsspec
@@ -61,6 +62,8 @@ def __init__(
6162 self .url = path
6263 self .specs = AttrDict ()
6364 self .children = AttrDict ()
65+ self .contents = AttrDict ()
66+ self .artifacts = AttrDict ()
6467 self .excludes = excludes or default_excludes
6568 self ._pyproject = None
6669 self .resolve (walk = walk , types = types , xtypes = xtypes )
@@ -90,17 +93,21 @@ def resolve(
9093 for name in sorted (registry ):
9194 cls = registry [name ]
9295 try :
93- logger .debug ("resolving %s as %s" , fullpath , cls )
9496 name = cls .__name__
9597 snake_name = camel_to_snake (cls .__name__ )
9698 if (types and {name , snake_name }.isdisjoint (types )) or {
9799 name ,
98100 snake_name ,
99101 }.intersection (xtypes or set ()):
100102 continue
103+ logger .debug ("resolving %s as %s" , fullpath , cls )
101104 inst = cls (self )
102105 inst .parse ()
103- self .specs [snake_name ] = inst
106+ if isinstance (inst , ProjectExtra ):
107+ self .contents .update (inst .contents )
108+ self .artifacts .update (inst .artifacts )
109+ else :
110+ self .specs [snake_name ] = inst
104111 except ParseFailed :
105112 logger .debug ("failed" )
106113 except Exception as e :
@@ -148,7 +155,7 @@ def text_summary(self) -> str:
148155 """Only shows project types, not what they contain"""
149156 txt = f"<Project '{ self .fs .unstrip_protocol (self .url )} '>\n "
150157 bits = [
151- f" { '/' } : { ' ' .join (type (_ ).__name__ for _ in self .specs .values ())} "
158+ f" { '/' } : { ' ' .join (type (_ ).__name__ for _ in chain ( self .specs .values (), self . contents . values (), self . artifacts . values () ))} "
152159 ] + [
153160 f" { k } : { ' ' .join (type (_ ).__name__ for _ in v .specs .values ())} "
154161 for k , v in self .children .items ()
@@ -160,6 +167,12 @@ def __repr__(self):
160167 self .fs .unstrip_protocol (self .url ),
161168 "\n \n " .join (str (_ ) for _ in self .specs .values ()),
162169 )
170+ if self .contents :
171+ ch = "\n " .join ([f" { k } : { v } " for k , v in self .contents .items ()])
172+ txt += f"\n Contents:\n { ch } "
173+ if self .artifacts :
174+ ch = "\n " .join ([f" { k } : { v } " for k , v in self .artifacts .items ()])
175+ txt += f"\n Artifacts:\n { ch } "
163176 if self .children :
164177 ch = "\n " .join (
165178 [
@@ -194,23 +207,32 @@ def pyproject(self):
194207 pass
195208 return {}
196209
197- @property
198- def artifacts (self ) -> set :
210+ def all_artifacts (self , names = None ) -> list :
199211 """A flat list of all the artifact objects nested in this project."""
200- arts = set ()
212+ arts = set (self . artifacts . values () )
201213 for spec in self .specs .values ():
202214 arts .update (flatten (spec .artifacts ))
203215 for child in self .children .values ():
204216 arts .update (child .artifacts )
217+ if names :
218+ if isinstance (names , str ):
219+ names = {names }
220+ arts = [
221+ a
222+ for a in arts
223+ if any (a .snake_name () == camel_to_snake (n ) for n in names )
224+ ]
225+ else :
226+ arts = list (arts )
205227 return arts
206228
207- def filter_by_type (self , types : Iterable [type ]) -> bool :
229+ def has_artifact_type (self , types : Iterable [type ]) -> bool :
208230 """Answers 'does this project support outputting the given artifact type'
209231
210232 This is an experimental example of filtering through projects
211233 """
212234 types = tuple (types )
213- return any (isinstance (_ , types ) for _ in self .artifacts )
235+ return any (isinstance (_ , types ) for _ in self .all_artifacts () )
214236
215237 def __contains__ (self , item ) -> bool :
216238 """Is the given project type supported ANYWHERE in this directory?"""
@@ -222,6 +244,8 @@ def to_dict(self, compact=True) -> dict:
222244 children = self .children ,
223245 url = self .url ,
224246 storage_options = self .storage_options ,
247+ artifacts = self .artifacts ,
248+ contents = self .contents ,
225249 )
226250 if not compact :
227251 dic ["klass" ] = "project"
@@ -346,11 +370,13 @@ def snake_name(cls) -> str:
346370class ProjectExtra (ProjectSpec ):
347371 """A special subcategory of project types with content but no structure.
348372
349- Subclasses of this are special, in that they are not free- standing projects, but add
373+ Subclasses of this are special: they are not free standing projects, but add
350374 contents onto the root project. Examples include data catalog specification, linters
351- and Ci /CD, that may be run against the root project without using a project-oriented
375+ and CI /CD, that may be run against the root project without using a project-oriented
352376 tool.
353377
354378 These classes do not appear in a Project's .specs, but do contribute .contents or
355379 .artifacts. They are still referenced when filtering spec types by name.
380+
381+ Commonly, subclasses are tied 1-1 to a particular content/artifact class.
356382 """
0 commit comments