fabricatio 0.3.14.dev7__cp313-cp313-win_amd64.whl → 0.3.15.dev5__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,10 +3,8 @@
3
3
  from abc import ABC
4
4
  from enum import StrEnum
5
5
  from pathlib import Path
6
- from pydantic import Field
7
6
  from typing import ClassVar, Generator, List, Optional, Self, Tuple, Type
8
7
 
9
- from fabricatio.capabilities.persist import PersistentAble
10
8
  from fabricatio.fs import dump_text, safe_text_read
11
9
  from fabricatio.fs.readers import extract_sections
12
10
  from fabricatio.journal import logger
@@ -17,13 +15,23 @@ from fabricatio.models.generic import (
17
15
  Introspect,
18
16
  Language,
19
17
  ModelHash,
18
+ PersistentAble,
20
19
  ProposedUpdateAble,
21
20
  SketchedAble,
22
21
  Titled,
23
22
  WordCount,
24
23
  )
25
- from fabricatio.rust import extract_body, replace_thesis_body, split_out_metadata, to_metadata, word_count
24
+ from fabricatio.rust import (
25
+ detect_language,
26
+ extract_body,
27
+ replace_thesis_body,
28
+ split_out_metadata,
29
+ strip_comment,
30
+ to_metadata,
31
+ word_count,
32
+ )
26
33
  from fabricatio.utils import fallback_kwargs, ok
34
+ from pydantic import Field
27
35
 
28
36
  ARTICLE_WRAPPER = "// =-=-=-=-=-=-=-=-=-="
29
37
 
@@ -52,10 +60,32 @@ class ArticleMetaData(SketchedAble, Described, WordCount, Titled, Language):
52
60
  aims: List[str]
53
61
  """List of writing aims of the research component in academic style."""
54
62
 
63
+ _unstructured_body: str = ""
64
+ """Store the source of the unknown information."""
65
+
55
66
  @property
56
67
  def typst_metadata_comment(self) -> str:
57
68
  """Generates a comment for the metadata of the article component."""
58
- return to_metadata(self.model_dump(include={"description", "aims", "expected_word_count"}, by_alias=True))
69
+ data = self.model_dump(
70
+ include={"description", "aims", "expected_word_count"},
71
+ by_alias=True,
72
+ )
73
+ return to_metadata({k: v for k, v in data.items() if v})
74
+
75
+ @property
76
+ def unstructured_body(self) -> str:
77
+ """Returns the unstructured body of the article component."""
78
+ return self._unstructured_body
79
+
80
+ def update_unstructured_body[S: "ArticleMetaData"](self: S, body: str) -> S:
81
+ """Update the unstructured body of the article component."""
82
+ self._unstructured_body = body
83
+ return self
84
+
85
+ @property
86
+ def language(self) -> str:
87
+ """Get the language of the article component."""
88
+ return detect_language(self.title)
59
89
 
60
90
 
61
91
  class FromTypstCode(ArticleMetaData):
@@ -67,13 +97,8 @@ class FromTypstCode(ArticleMetaData):
67
97
  data, body = split_out_metadata(body)
68
98
 
69
99
  return cls(
70
- heading=title,
71
- **fallback_kwargs(
72
- data or {},
73
- elaboration="",
74
- expected_word_count=word_count(body),
75
- aims=[],
76
- ),
100
+ heading=title.strip(),
101
+ **fallback_kwargs(data or {}, elaboration="", expected_word_count=word_count(body), aims=[]),
77
102
  **kwargs,
78
103
  )
79
104
 
@@ -83,7 +108,7 @@ class ToTypstCode(ArticleMetaData):
83
108
 
84
109
  def to_typst_code(self) -> str:
85
110
  """Converts the component into a Typst code snippet for rendering."""
86
- return f"{self.title}\n{self.typst_metadata_comment}\n"
111
+ return f"{self.title}\n{self.typst_metadata_comment}\n\n{self._unstructured_body}"
87
112
 
88
113
 
89
114
  class ArticleOutlineBase(
@@ -151,12 +176,16 @@ class SectionBase[T: SubSectionBase](ArticleOutlineBase):
151
176
  @classmethod
152
177
  def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
153
178
  """Creates an Article object from the given Typst code."""
154
- return super().from_typst_code(
155
- title,
156
- body,
157
- subsections=[
158
- cls.child_type.from_typst_code(*pack) for pack in extract_sections(body, level=3, section_char="=")
159
- ],
179
+ raw = extract_sections(body, level=3, section_char="=")
180
+
181
+ return (
182
+ super()
183
+ .from_typst_code(
184
+ title,
185
+ body,
186
+ subsections=[cls.child_type.from_typst_code(*pack) for pack in raw],
187
+ )
188
+ .update_unstructured_body("" if raw else strip_comment(body))
160
189
  )
161
190
 
162
191
  def resolve_update_conflict(self, other: Self) -> str:
@@ -191,6 +220,11 @@ class SectionBase[T: SubSectionBase](ArticleOutlineBase):
191
220
  return f"Section `{self.title}` contains no subsections, expected at least one, but got 0, you can add one or more as needed."
192
221
  return ""
193
222
 
223
+ @property
224
+ def exact_word_count(self) -> int:
225
+ """Returns the exact word count of the article section outline."""
226
+ return sum(a.exact_word_count for a in self.subsections)
227
+
194
228
 
195
229
  class ChapterBase[T: SectionBase](ArticleOutlineBase):
196
230
  """Base class for article chapters."""
@@ -206,12 +240,16 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
206
240
  @classmethod
207
241
  def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
208
242
  """Creates an Article object from the given Typst code."""
209
- return super().from_typst_code(
210
- title,
211
- body,
212
- sections=[
213
- cls.child_type.from_typst_code(*pack) for pack in extract_sections(body, level=2, section_char="=")
214
- ],
243
+ raw_sec = extract_sections(body, level=2, section_char="=")
244
+
245
+ return (
246
+ super()
247
+ .from_typst_code(
248
+ title,
249
+ body,
250
+ sections=[cls.child_type.from_typst_code(*pack) for pack in raw_sec],
251
+ )
252
+ .update_unstructured_body("" if raw_sec else strip_comment(body))
215
253
  )
216
254
 
217
255
  def resolve_update_conflict(self, other: Self) -> str:
@@ -243,6 +281,15 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
243
281
  return f"Chapter `{self.title}` contains no sections, expected at least one, but got 0, you can add one or more as needed."
244
282
  return ""
245
283
 
284
+ @property
285
+ def exact_word_count(self) -> int:
286
+ """Calculates the total word count across all sections in the chapter.
287
+
288
+ Returns:
289
+ int: The cumulative word count of all sections.
290
+ """
291
+ return sum(a.exact_word_count for a in self.sections)
292
+
246
293
 
247
294
  class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, ToTypstCode, ABC):
248
295
  """Base class for article outlines."""
@@ -263,19 +310,38 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, To
263
310
 
264
311
  child_type: ClassVar[Type[ChapterBase]]
265
312
 
313
+ @property
314
+ def language(self) -> str:
315
+ """Get the language of the article."""
316
+ if self.title:
317
+ return super().language
318
+ return self.chapters[0].language
319
+
320
+ @property
321
+ def exact_word_count(self) -> int:
322
+ """Calculates the total word count across all chapters in the article.
323
+
324
+ Returns:
325
+ int: The cumulative word count of all chapters.
326
+ """
327
+ return sum(ch.exact_word_count for ch in self.chapters)
328
+
266
329
  @classmethod
267
330
  def from_typst_code(cls, title: str, body: str, **kwargs) -> Self:
268
331
  """Generates an article from the given Typst code."""
269
- return super().from_typst_code(
270
- title,
271
- body,
272
- chapters=[
273
- cls.child_type.from_typst_code(*pack) for pack in extract_sections(body, level=1, section_char="=")
274
- ],
332
+ raw = extract_sections(body, level=1, section_char="=")
333
+ return (
334
+ super()
335
+ .from_typst_code(
336
+ title,
337
+ body,
338
+ chapters=[cls.child_type.from_typst_code(*pack) for pack in raw],
339
+ )
340
+ .update_unstructured_body("" if raw else strip_comment(body))
275
341
  )
276
342
 
277
343
  def iter_dfs_rev(
278
- self,
344
+ self,
279
345
  ) -> Generator[ArticleOutlineBase, None, None]:
280
346
  """Performs a depth-first search (DFS) through the article structure in reverse order.
281
347
 
@@ -350,7 +416,7 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, To
350
416
 
351
417
  def to_typst_code(self) -> str:
352
418
  """Generates the Typst code representation of the article."""
353
- return f"// #{super().to_typst_code()}\n\n" + "\n\n".join(a.to_typst_code() for a in self.chapters)
419
+ return f"// #Title: {super().to_typst_code()}\n" + "\n\n".join(a.to_typst_code() for a in self.chapters)
354
420
 
355
421
  def finalized_dump(self) -> str:
356
422
  """Generates standardized hierarchical markup for academic publishing systems.
@@ -401,11 +467,11 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, To
401
467
  """Set all chap, sec, subsec have same word count sum up to be `self.expected_word_count`."""
402
468
  return self.avg_chap_wordcount().avg_sec_wordcount().avg_subsec_wordcount()
403
469
 
404
- def update_article_file(self, file: str | Path) -> Self:
470
+ def update_article_file[S: "ArticleBase"](self: S, file: str | Path) -> S:
405
471
  """Update the article file."""
406
472
  file = Path(file)
407
473
  string = safe_text_read(file)
408
- if updated := replace_thesis_body(string, ARTICLE_WRAPPER, self.to_typst_code()):
474
+ if updated := replace_thesis_body(string, ARTICLE_WRAPPER, f"\n\n{self.to_typst_code()}\n\n"):
409
475
  dump_text(file, updated)
410
476
  logger.success(f"Successfully updated {file.as_posix()}.")
411
477
  else:
@@ -413,7 +479,7 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, FromTypstCode, To
413
479
  return self
414
480
 
415
481
  @classmethod
416
- def from_article_file[S: "ArticleBase"](cls: Type[S], file: str | Path, title: str) -> S:
482
+ def from_article_file[S: "ArticleBase"](cls: Type[S], file: str | Path, title: str = "") -> S:
417
483
  """Load article from file."""
418
484
  file = Path(file)
419
485
  string = safe_text_read(file)
@@ -2,9 +2,8 @@
2
2
 
3
3
  from typing import List
4
4
 
5
- from fabricatio.capabilities.persist import PersistentAble
6
5
  from fabricatio.models.extra.rag import MilvusDataBase
7
- from fabricatio.models.generic import SketchedAble
6
+ from fabricatio.models.generic import PersistentAble, SketchedAble
8
7
  from pydantic import BaseModel
9
8
 
10
9
 
@@ -97,5 +96,3 @@ class ArticleEssence(SketchedAble, PersistentAble, MilvusDataBase):
97
96
 
98
97
  def _prepare_vectorization_inner(self) -> str:
99
98
  return self.compact()
100
-
101
-
@@ -1,9 +1,7 @@
1
1
  """ArticleBase and ArticleSubsection classes for managing hierarchical document components."""
2
2
 
3
- from pydantic import Field, NonNegativeInt
4
3
  from typing import ClassVar, Dict, Generator, List, Self, Tuple, Type, override
5
4
 
6
- from fabricatio.capabilities.persist import PersistentAble
7
5
  from fabricatio.decorators import precheck_package
8
6
  from fabricatio.journal import logger
9
7
  from fabricatio.models.extra.article_base import (
@@ -18,13 +16,14 @@ from fabricatio.models.extra.article_outline import (
18
16
  ArticleSectionOutline,
19
17
  ArticleSubsectionOutline,
20
18
  )
21
- from fabricatio.models.generic import Described, SequencePatch, SketchedAble, WithRef, WordCount
19
+ from fabricatio.models.generic import Described, PersistentAble, SequencePatch, SketchedAble, WithRef, WordCount
22
20
  from fabricatio.rust import (
23
21
  convert_all_tex_math,
24
22
  fix_misplaced_labels,
25
23
  split_out_metadata,
26
24
  word_count,
27
25
  )
26
+ from pydantic import Field, NonNegativeInt
28
27
 
29
28
  PARAGRAPH_SEP = "// - - -"
30
29
 
@@ -52,7 +51,7 @@ class Paragraph(SketchedAble, WordCount, Described):
52
51
  return cls(elaboration="", aims=[], expected_word_count=word_count(content), content=content.strip())
53
52
 
54
53
  @property
55
- def exact_wordcount(self) -> int:
54
+ def exact_word_count(self) -> int:
56
55
  """Calculates the exact word count of the content."""
57
56
  return word_count(self.content)
58
57
 
@@ -70,6 +69,11 @@ class ArticleSubsection(SubSectionBase):
70
69
  _max_word_count_deviation: float = 0.3
71
70
  """Maximum allowed deviation from the expected word count, as a percentage."""
72
71
 
72
+ @property
73
+ def exact_word_count(self) -> int:
74
+ """Calculates the exact word count of all paragraphs in the subsection."""
75
+ return sum(a.exact_word_count for a in self.paragraphs)
76
+
73
77
  @property
74
78
  def word_count(self) -> int:
75
79
  """Calculates the total word count of all paragraphs in the subsection."""
@@ -81,8 +85,8 @@ class ArticleSubsection(SubSectionBase):
81
85
  if len(self.paragraphs) == 0:
82
86
  summary += f"`{self.__class__.__name__}` titled `{self.title}` have no paragraphs, You should add some!\n"
83
87
  if (
84
- abs((wc := self.word_count) - self.expected_word_count) / self.expected_word_count
85
- > self._max_word_count_deviation
88
+ abs((wc := self.word_count) - self.expected_word_count) / self.expected_word_count
89
+ > self._max_word_count_deviation
86
90
  ):
87
91
  summary += f"`{self.__class__.__name__}` titled `{self.title}` have {wc} words, expected {self.expected_word_count} words!"
88
92
 
@@ -273,9 +277,9 @@ class Article(
273
277
  err = []
274
278
  for chap, sec, subsec in self.iter_subsections():
275
279
  for i, p in enumerate(subsec.paragraphs):
276
- if p.exact_wordcount <= threshold:
280
+ if p.exact_word_count <= threshold:
277
281
  err.append(
278
- f"{chap.title}->{sec.title}->{subsec.title}-> Paragraph [{i}] is too short, {p.exact_wordcount} words."
282
+ f"{chap.title}->{sec.title}->{subsec.title}-> Paragraph [{i}] is too short, {p.exact_word_count} words."
279
283
  )
280
284
 
281
285
  return "\n".join(err)
@@ -2,7 +2,6 @@
2
2
 
3
3
  from typing import ClassVar, Dict, Type
4
4
 
5
- from fabricatio.capabilities.persist import PersistentAble
6
5
  from fabricatio.models.extra.article_base import (
7
6
  ArticleBase,
8
7
  ChapterBase,
@@ -10,7 +9,7 @@ from fabricatio.models.extra.article_base import (
10
9
  SubSectionBase,
11
10
  )
12
11
  from fabricatio.models.extra.article_proposal import ArticleProposal
13
- from fabricatio.models.generic import WithRef
12
+ from fabricatio.models.generic import PersistentAble, WithRef
14
13
 
15
14
 
16
15
  class ArticleSubsectionOutline(SubSectionBase):
@@ -2,11 +2,11 @@
2
2
 
3
3
  from typing import Dict, List
4
4
 
5
- from fabricatio.capabilities.persist import PersistentAble
6
5
  from fabricatio.models.generic import (
7
6
  AsPrompt,
8
7
  Described,
9
8
  Language,
9
+ PersistentAble,
10
10
  SketchedAble,
11
11
  Titled,
12
12
  WithRef,
@@ -10,8 +10,7 @@ complex rule management systems.
10
10
 
11
11
  from typing import List, Self, Tuple, Unpack
12
12
 
13
- from fabricatio.capabilities.persist import PersistentAble
14
- from fabricatio.models.generic import Language, SketchedAble, WithBriefing
13
+ from fabricatio.models.generic import Language, PersistentAble, SketchedAble, WithBriefing
15
14
  from more_itertools import flatten
16
15
 
17
16
 
@@ -1,6 +1,7 @@
1
1
  """This module defines generic classes for models in the Fabricatio library, providing a foundation for various model functionalities."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
+ from datetime import datetime
4
5
  from pathlib import Path
5
6
  from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Self, Sequence, Type, Union, final, overload
6
7
 
@@ -114,7 +115,7 @@ class WordCount(Base, ABC):
114
115
  @property
115
116
  def exact_word_count(self) -> int:
116
117
  """Get the exact word count of this research component."""
117
- raise NotImplementedError(f"`expected_word_count` is not implemented for {self.__class__.__name__}")
118
+ raise NotImplementedError(f"`exact_word_count` is not implemented for {self.__class__.__name__}")
118
119
 
119
120
 
120
121
  class FromMapping:
@@ -810,3 +811,94 @@ class SequencePatch[T](ProposedUpdateAble, ABC):
810
811
  Self: A new instance with an empty list of tweaks.
811
812
  """
812
813
  return cls(tweaked=[])
814
+
815
+
816
+ class PersistentAble(Base, ABC):
817
+ """Class providing file persistence capabilities.
818
+
819
+ Enables saving model instances to disk with timestamped filenames and loading from persisted files.
820
+ Implements basic versioning through filename hashing and timestamping.
821
+ """
822
+
823
+ def persist(self, path: str | Path) -> Self:
824
+ """Save model instance to disk with versioned filename.
825
+
826
+ Args:
827
+ path (str | Path): Target directory or file path. If directory, filename is auto-generated.
828
+
829
+ Returns:
830
+ Self: Current instance for method chaining
831
+
832
+ Notes:
833
+ - Filename format: <ClassName>_<YYYYMMDD_HHMMSS>_<6-char_hash>.json
834
+ - Hash generated from JSON content ensures uniqueness
835
+ """
836
+ p = Path(path)
837
+ out = self.model_dump_json(indent=1, by_alias=True)
838
+
839
+ # Generate a timestamp in the format YYYYMMDD_HHMMSS
840
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
841
+
842
+ # Generate the hash
843
+ file_hash = blake3_hash(out.encode())[:6]
844
+
845
+ # Construct the file name with timestamp and hash
846
+ file_name = f"{self.__class__.__name__}_{timestamp}_{file_hash}.json"
847
+
848
+ if p.is_dir():
849
+ p.joinpath(file_name).write_text(out, encoding="utf-8")
850
+ else:
851
+ p.mkdir(exist_ok=True, parents=True)
852
+ p.write_text(out, encoding="utf-8")
853
+
854
+ logger.info(f"Persisted `{self.__class__.__name__}` to {p.as_posix()}")
855
+ return self
856
+
857
+ @classmethod
858
+ def from_latest_persistent(cls, dir_path: str | Path) -> Optional[Self]:
859
+ """Load most recent persisted instance from directory.
860
+
861
+ Args:
862
+ dir_path (str | Path): Directory containing persisted files
863
+
864
+ Returns:
865
+ Self: Most recently modified instance
866
+
867
+ Raises:
868
+ NotADirectoryError: If path is not a valid directory
869
+ FileNotFoundError: If no matching files found
870
+ """
871
+ dir_path = Path(dir_path)
872
+ if not dir_path.is_dir():
873
+ return None
874
+
875
+ pattern = f"{cls.__name__}_*.json"
876
+ files = list(dir_path.glob(pattern))
877
+
878
+ if not files:
879
+ return None
880
+
881
+ def _get_timestamp(file_path: Path) -> datetime:
882
+ stem = file_path.stem
883
+ parts = stem.split("_")
884
+ return datetime.strptime(f"{parts[1]}_{parts[2]}", "%Y%m%d_%H%M%S")
885
+
886
+ files.sort(key=lambda f: _get_timestamp(f), reverse=True)
887
+
888
+ return cls.from_persistent(files.pop(0))
889
+
890
+ @classmethod
891
+ def from_persistent(cls, path: str | Path) -> Self:
892
+ """Load an instance from a specific persisted file.
893
+
894
+ Args:
895
+ path (str | Path): Path to the JSON file.
896
+
897
+ Returns:
898
+ Self: The loaded instance from the file.
899
+
900
+ Raises:
901
+ FileNotFoundError: If the specified file does not exist.
902
+ ValueError: If the file content is invalid for the model.
903
+ """
904
+ return cls.model_validate_json(safe_text_read(path))
fabricatio/models/role.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Module that contains the Role class for managing workflows and their event registrations."""
2
2
 
3
3
  from functools import partial
4
- from typing import Any, Dict, Self
4
+ from typing import Any, Callable, Dict, Self, Type
5
5
 
6
6
  from fabricatio.emitter import env
7
7
  from fabricatio.journal import logger
@@ -30,6 +30,8 @@ class Role(WithBriefing):
30
30
 
31
31
  registry: Dict[Event, WorkFlow] = Field(default_factory=dict)
32
32
  """The registry of events and workflows."""
33
+ dispatch_on_init: bool = True
34
+ """Whether to dispatch registered workflows on initialization."""
33
35
 
34
36
  def model_post_init(self, __context: Any) -> None:
35
37
  """Initialize the role by resolving configurations and registering workflows.
@@ -39,9 +41,20 @@ class Role(WithBriefing):
39
41
  """
40
42
  self.name = self.name or self.__class__.__name__
41
43
 
42
- self.resolve_configuration().register_workflows()
44
+ if self.dispatch_on_init:
45
+ self.resolve_configuration().dispatch()
46
+
47
+ def register_workflow(self, event: Event, workflow: WorkFlow) -> Self:
48
+ """Register a workflow to the role's registry."""
49
+ if event in self.registry:
50
+ logger.warning(
51
+ f"Event `{event.collapse()}` is already registered with workflow "
52
+ f"`{self.registry[event].name}`. It will be overwritten by `{workflow.name}`."
53
+ )
54
+ self.registry[event] = workflow
55
+ return self
43
56
 
44
- def register_workflows(self) -> Self:
57
+ def dispatch(self) -> Self:
45
58
  """Register each workflow in the registry to its corresponding event in the event bus.
46
59
 
47
60
  Returns:
@@ -63,33 +76,81 @@ class Role(WithBriefing):
63
76
  """
64
77
  for workflow in self.registry.values():
65
78
  logger.debug(f"Resolving config for workflow: `{workflow.name}`")
66
- self._configure_scoped_config(workflow)
67
- self._configure_toolbox_usage(workflow)
79
+ self._configure_scoped_config(workflow)._configure_toolbox_usage(workflow)
80
+
68
81
  workflow.inject_personality(self.briefing)
69
82
  return self
70
83
 
71
- def _configure_scoped_config(self, workflow: WorkFlow) -> None:
72
- """Configure scoped configuration for workflow and its actions."""
73
- if not is_scoped_config(self.__class__):
74
- return
84
+ def _propagate_config(
85
+ self,
86
+ workflow: WorkFlow,
87
+ has_capability: Callable[[Type], bool],
88
+ config_method_name: str,
89
+ capability_description: str,
90
+ ) -> Self:
91
+ """Propagates configuration from the Role to a Workflow and its Actions.
92
+
93
+ This method checks if the Role, Workflow, or its Actions possess a specific
94
+ capability (e.g., being a ScopedConfig or ToolBoxUsage). If they do,
95
+ a specified configuration method is called on them to apply or inherit
96
+ settings.
97
+
98
+ The configuration flows hierarchically:
99
+ 1. If the Role has the capability, it's the initial source.
100
+ 2. If the Workflow also has the capability, it can inherit from the Role
101
+ and then becomes the source for its Actions.
102
+ 3. Actions with the capability inherit from the determined source (either
103
+ Workflow or Role).
75
104
 
76
- fallback_target = self
77
- if is_scoped_config(workflow):
78
- workflow.fallback_to(self)
79
- fallback_target = workflow
80
-
81
- for action in (a for a in workflow.iter_actions() if is_scoped_config(a)):
82
- action.fallback_to(fallback_target)
105
+ Args:
106
+ workflow: The WorkFlow instance to configure.
107
+ has_capability: A callable that takes a Type and returns True if
108
+ the type possesses the specific capability, False otherwise.
109
+ config_method_name: The name of the method to call on an object
110
+ (Role, Workflow, Action) to apply the configuration.
111
+ For example, "fallback_to" or "supply_tools_from".
112
+ capability_description: A string describing the capability, used for
113
+ logging purposes (e.g., "scoped config", "toolbox usage").
114
+ """
115
+ # This variable will hold the object from which Actions should inherit their configuration.
116
+ # It could be the Role itself or the Workflow, depending on their capabilities.
117
+ config_source_for_actions = None
118
+
119
+ # Check if the Role itself has the capability.
120
+ if has_capability(self.__class__):
121
+ # If the Role has the capability, it becomes the initial source for configuration.
122
+ config_source_for_actions = self
123
+
124
+ # Check if the Workflow has the capability.
125
+ if has_capability(workflow.__class__):
126
+ logger.debug(
127
+ f"Configuring {capability_description} inherited from `{self.name}` for workflow: `{workflow.name}`"
128
+ )
129
+ # If the Role was already identified as a config source,
130
+ # the Workflow an inherit its configuration directly from the Role.
131
+ if config_source_for_actions is not None:
132
+ # Call the specified configuration method on the workflow, passing the Role (self) as the source.
133
+ getattr(workflow, config_method_name)(config_source_for_actions)
134
+
135
+ # After potentially inheriting from the Role, the Workflow itself becomes
136
+ # the source of configuration for its Actions.
137
+ config_source_for_actions = workflow
138
+
139
+ # If a configuration source (either Role or Workflow) has been established:
140
+ if config_source_for_actions is not None:
141
+ # Iterate over all actions within the workflow.
142
+ # Filter for actions that possess the specified capability.
143
+ for action in (act for act in workflow.iter_actions() if has_capability(act.__class__)):
144
+ # Call the specified configuration method on the action,
145
+ # passing the determined config_source_for_actions.
146
+ getattr(action, config_method_name)(config_source_for_actions)
83
147
 
84
- def _configure_toolbox_usage(self, workflow: WorkFlow) -> None:
85
- """Configure toolbox usage for workflow and its actions."""
86
- if not is_toolbox_usage(self.__class__):
87
- return
148
+ return self
88
149
 
89
- supply_target = self
90
- if is_toolbox_usage(workflow):
91
- workflow.supply_tools_from(self)
92
- supply_target = workflow
150
+ def _configure_scoped_config(self, workflow: WorkFlow) -> Self:
151
+ """Configure scoped configuration for workflow and its actions."""
152
+ return self._propagate_config(workflow, is_scoped_config, "fallback_to", "scoped config")
93
153
 
94
- for action in (a for a in workflow.iter_actions() if is_toolbox_usage(a)):
95
- action.supply_tools_from(supply_target)
154
+ def _configure_toolbox_usage(self, workflow: WorkFlow) -> Self:
155
+ """Configure toolbox usage for workflow and its actions."""
156
+ return self._propagate_config(workflow, is_toolbox_usage, "supply_tools_from", "toolbox usage")
Binary file