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.
- fabricatio/actions/article.py +115 -19
- fabricatio/actions/article_rag.py +52 -52
- fabricatio/actions/output.py +21 -22
- fabricatio/decorators.py +2 -0
- fabricatio/models/extra/aricle_rag.py +4 -5
- fabricatio/models/extra/article_base.py +101 -35
- fabricatio/models/extra/article_essence.py +1 -4
- fabricatio/models/extra/article_main.py +12 -8
- fabricatio/models/extra/article_outline.py +1 -2
- fabricatio/models/extra/article_proposal.py +1 -1
- fabricatio/models/extra/rule.py +1 -2
- fabricatio/models/generic.py +93 -1
- fabricatio/models/role.py +87 -26
- fabricatio/rust.cp313-win_amd64.pyd +0 -0
- fabricatio/rust.pyi +20 -61
- fabricatio-0.3.15.dev5.data/scripts/tdown.exe +0 -0
- fabricatio-0.3.15.dev5.data/scripts/ttm.exe +0 -0
- {fabricatio-0.3.14.dev7.dist-info → fabricatio-0.3.15.dev5.dist-info}/METADATA +3 -1
- {fabricatio-0.3.14.dev7.dist-info → fabricatio-0.3.15.dev5.dist-info}/RECORD +21 -22
- {fabricatio-0.3.14.dev7.dist-info → fabricatio-0.3.15.dev5.dist-info}/WHEEL +1 -1
- fabricatio/capabilities/persist.py +0 -103
- fabricatio-0.3.14.dev7.data/scripts/tdown.exe +0 -0
- fabricatio-0.3.14.dev7.data/scripts/ttm.exe +0 -0
- {fabricatio-0.3.14.dev7.dist-info → fabricatio-0.3.15.dev5.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
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
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
-
|
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
|
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) ->
|
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
|
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
|
-
|
85
|
-
|
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.
|
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.
|
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):
|
fabricatio/models/extra/rule.py
CHANGED
@@ -10,8 +10,7 @@ complex rule management systems.
|
|
10
10
|
|
11
11
|
from typing import List, Self, Tuple, Unpack
|
12
12
|
|
13
|
-
from fabricatio.
|
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
|
|
fabricatio/models/generic.py
CHANGED
@@ -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"`
|
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.
|
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
|
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
|
-
|
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
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
95
|
-
|
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
|