donna 0.2.0__py3-none-any.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.
Files changed (85) hide show
  1. donna/__init__.py +1 -0
  2. donna/artifacts/__init__.py +0 -0
  3. donna/artifacts/usage/__init__.py +0 -0
  4. donna/artifacts/usage/artifacts.md +224 -0
  5. donna/artifacts/usage/cli.md +117 -0
  6. donna/artifacts/usage/worlds.md +36 -0
  7. donna/artifacts/work/__init__.py +0 -0
  8. donna/artifacts/work/do_it.md +142 -0
  9. donna/artifacts/work/do_it_fast.md +98 -0
  10. donna/artifacts/work/planning.md +245 -0
  11. donna/cli/__init__.py +0 -0
  12. donna/cli/__main__.py +6 -0
  13. donna/cli/application.py +17 -0
  14. donna/cli/commands/__init__.py +0 -0
  15. donna/cli/commands/artifacts.py +110 -0
  16. donna/cli/commands/projects.py +49 -0
  17. donna/cli/commands/sessions.py +77 -0
  18. donna/cli/types.py +138 -0
  19. donna/cli/utils.py +53 -0
  20. donna/core/__init__.py +0 -0
  21. donna/core/entities.py +27 -0
  22. donna/core/errors.py +126 -0
  23. donna/core/result.py +99 -0
  24. donna/core/utils.py +37 -0
  25. donna/domain/__init__.py +0 -0
  26. donna/domain/errors.py +47 -0
  27. donna/domain/ids.py +497 -0
  28. donna/lib/__init__.py +21 -0
  29. donna/lib/sources.py +5 -0
  30. donna/lib/worlds.py +7 -0
  31. donna/machine/__init__.py +0 -0
  32. donna/machine/action_requests.py +50 -0
  33. donna/machine/artifacts.py +200 -0
  34. donna/machine/changes.py +91 -0
  35. donna/machine/errors.py +122 -0
  36. donna/machine/operations.py +31 -0
  37. donna/machine/primitives.py +77 -0
  38. donna/machine/sessions.py +215 -0
  39. donna/machine/state.py +244 -0
  40. donna/machine/tasks.py +89 -0
  41. donna/machine/templates.py +83 -0
  42. donna/primitives/__init__.py +1 -0
  43. donna/primitives/artifacts/__init__.py +0 -0
  44. donna/primitives/artifacts/specification.py +20 -0
  45. donna/primitives/artifacts/workflow.py +195 -0
  46. donna/primitives/directives/__init__.py +0 -0
  47. donna/primitives/directives/goto.py +44 -0
  48. donna/primitives/directives/task_variable.py +73 -0
  49. donna/primitives/directives/view.py +45 -0
  50. donna/primitives/operations/__init__.py +0 -0
  51. donna/primitives/operations/finish_workflow.py +37 -0
  52. donna/primitives/operations/request_action.py +89 -0
  53. donna/primitives/operations/run_script.py +250 -0
  54. donna/protocol/__init__.py +0 -0
  55. donna/protocol/cell_shortcuts.py +9 -0
  56. donna/protocol/cells.py +44 -0
  57. donna/protocol/errors.py +17 -0
  58. donna/protocol/formatters/__init__.py +0 -0
  59. donna/protocol/formatters/automation.py +25 -0
  60. donna/protocol/formatters/base.py +15 -0
  61. donna/protocol/formatters/human.py +36 -0
  62. donna/protocol/formatters/llm.py +39 -0
  63. donna/protocol/modes.py +40 -0
  64. donna/protocol/nodes.py +59 -0
  65. donna/world/__init__.py +0 -0
  66. donna/world/artifacts.py +122 -0
  67. donna/world/artifacts_discovery.py +90 -0
  68. donna/world/config.py +198 -0
  69. donna/world/errors.py +232 -0
  70. donna/world/initialization.py +42 -0
  71. donna/world/markdown.py +267 -0
  72. donna/world/sources/__init__.py +1 -0
  73. donna/world/sources/base.py +62 -0
  74. donna/world/sources/markdown.py +260 -0
  75. donna/world/templates.py +181 -0
  76. donna/world/tmp.py +33 -0
  77. donna/world/worlds/__init__.py +0 -0
  78. donna/world/worlds/base.py +68 -0
  79. donna/world/worlds/filesystem.py +189 -0
  80. donna/world/worlds/python.py +196 -0
  81. donna-0.2.0.dist-info/METADATA +44 -0
  82. donna-0.2.0.dist-info/RECORD +85 -0
  83. donna-0.2.0.dist-info/WHEEL +4 -0
  84. donna-0.2.0.dist-info/entry_points.txt +3 -0
  85. donna-0.2.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,267 @@
1
+ import enum
2
+ from typing import Any
3
+
4
+ import pydantic
5
+ from markdown_it import MarkdownIt
6
+ from markdown_it.token import Token
7
+ from markdown_it.tree import SyntaxTreeNode
8
+ from mdformat.renderer import MDRenderer
9
+
10
+ from donna.core.entities import BaseEntity
11
+ from donna.core.errors import ErrorsList
12
+ from donna.core.result import Err, Ok, Result, unwrap_to_error
13
+ from donna.domain.ids import FullArtifactId
14
+ from donna.world import errors as world_errors
15
+
16
+
17
+ class SectionLevel(str, enum.Enum):
18
+ h1 = "h1"
19
+ h2 = "h2"
20
+
21
+
22
+ class CodeSource(BaseEntity):
23
+ format: str
24
+ properties: dict[str, str | bool]
25
+ content: str
26
+
27
+ def structured_data(self) -> Result[Any, ErrorsList]:
28
+ if "script" in self.properties:
29
+ return Ok({})
30
+
31
+ if self.format == "json":
32
+ import json
33
+
34
+ return Ok(json.loads(self.content))
35
+
36
+ if self.format == "yaml" or self.format == "yml":
37
+ import yaml
38
+
39
+ return Ok(yaml.safe_load(self.content))
40
+
41
+ if self.format == "toml":
42
+ import tomllib
43
+
44
+ return Ok(tomllib.loads(self.content))
45
+
46
+ return Err([world_errors.MarkdownUnsupportedCodeFormat(format=self.format)])
47
+
48
+
49
+ class SectionSource(BaseEntity):
50
+ level: SectionLevel
51
+ title: str | None
52
+ configs: list[CodeSource]
53
+
54
+ original_tokens: list[Token]
55
+ analysis_tokens: list[Token]
56
+
57
+ model_config = pydantic.ConfigDict(frozen=False)
58
+
59
+ def _as_markdown(self, tokens: list[Token], with_title: bool) -> str:
60
+ parts = []
61
+
62
+ if with_title and self.title is not None:
63
+ match self.level:
64
+ case SectionLevel.h1:
65
+ prefix = "#"
66
+ case SectionLevel.h2:
67
+ prefix = "##"
68
+
69
+ parts.append(f"{prefix} {self.title}")
70
+
71
+ parts.append(render_back(tokens))
72
+
73
+ return "\n".join(parts)
74
+
75
+ def as_original_markdown(self, with_title: bool) -> str:
76
+ return self._as_markdown(self.original_tokens, with_title)
77
+
78
+ def as_analysis_markdown(self, with_title: bool) -> str:
79
+ return self._as_markdown(self.analysis_tokens, with_title)
80
+
81
+ def merged_configs(self) -> Result[dict[str, Any], ErrorsList]:
82
+ result: dict[str, Any] = {}
83
+ errors: ErrorsList = []
84
+
85
+ for config in self.configs:
86
+ data_result = config.structured_data()
87
+ if data_result.is_err():
88
+ errors.extend(data_result.unwrap_err())
89
+ continue
90
+ result.update(data_result.unwrap())
91
+
92
+ if errors:
93
+ return Err(errors)
94
+
95
+ return Ok(result)
96
+
97
+ def scripts(self) -> list[str]:
98
+ return [config.content for config in self.configs if "script" in config.properties]
99
+
100
+
101
+ def render_back(tokens: list[Token]) -> str:
102
+ renderer = MDRenderer()
103
+ return renderer.render(tokens, {}, {})
104
+
105
+
106
+ def clear_heading(text: str) -> str:
107
+ return text.lstrip("#").strip()
108
+
109
+
110
+ def _parse_h1(
111
+ sections: list[SectionSource], node: SyntaxTreeNode, artifact_id: FullArtifactId | None
112
+ ) -> Result[SyntaxTreeNode | None, ErrorsList]:
113
+ section = sections[-1]
114
+
115
+ if section.level != SectionLevel.h1:
116
+ return Err([world_errors.MarkdownMultipleH1Sections(artifact_id=artifact_id)])
117
+
118
+ if section.title is not None:
119
+ return Err([world_errors.MarkdownMultipleH1Titles(artifact_id=artifact_id)])
120
+
121
+ section.title = clear_heading(render_back(node.to_tokens()).strip())
122
+
123
+ return Ok(node.next_sibling)
124
+
125
+
126
+ def _parse_h2(
127
+ sections: list[SectionSource], node: SyntaxTreeNode, artifact_id: FullArtifactId | None
128
+ ) -> Result[SyntaxTreeNode | None, ErrorsList]:
129
+ section = sections[-1]
130
+
131
+ if section.title is None:
132
+ return Err([world_errors.MarkdownH2BeforeH1Title(artifact_id=artifact_id)])
133
+
134
+ new_section = SectionSource(
135
+ level=SectionLevel.h2,
136
+ title=clear_heading(render_back(node.to_tokens()).strip()),
137
+ original_tokens=[],
138
+ analysis_tokens=[],
139
+ configs=[],
140
+ )
141
+
142
+ sections.append(new_section)
143
+
144
+ return Ok(node.next_sibling)
145
+
146
+
147
+ def _parse_heading(
148
+ sections: list[SectionSource], node: SyntaxTreeNode, artifact_id: FullArtifactId | None
149
+ ) -> Result[SyntaxTreeNode | None, ErrorsList]:
150
+ section = sections[-1]
151
+
152
+ if node.tag == "h1":
153
+ return _parse_h1(sections, node, artifact_id)
154
+
155
+ if node.tag == "h2":
156
+ return _parse_h2(sections, node, artifact_id)
157
+
158
+ section.original_tokens.extend(node.to_tokens())
159
+ return Ok(node.next_sibling)
160
+
161
+
162
+ def _parse_fence(sections: list[SectionSource], node: SyntaxTreeNode) -> SyntaxTreeNode | None: # noqa: CCR001
163
+ section = sections[-1]
164
+
165
+ info_parts = node.info.split()
166
+
167
+ format = info_parts[0] if info_parts else ""
168
+
169
+ properties: dict[str, str | bool] = {}
170
+
171
+ for part in info_parts[1:]:
172
+ if "=" in part:
173
+ key, value = part.split("=", 1)
174
+ properties[key] = value
175
+ continue
176
+
177
+ properties[part] = True
178
+
179
+ if "donna" in properties:
180
+ code_block = CodeSource(
181
+ format=format,
182
+ properties=properties,
183
+ content=node.content,
184
+ )
185
+
186
+ section.configs.append(code_block)
187
+ else:
188
+ section.original_tokens.extend(node.to_tokens())
189
+
190
+ return node.next_sibling
191
+
192
+
193
+ def _parse_nested(sections: list[SectionSource], node: SyntaxTreeNode) -> SyntaxTreeNode | None:
194
+ section = sections[-1]
195
+
196
+ assert node.nester_tokens is not None
197
+
198
+ section.original_tokens.append(node.nester_tokens.opening)
199
+
200
+ return node.children[0]
201
+
202
+
203
+ def _parse_others(sections: list[SectionSource], node: SyntaxTreeNode) -> SyntaxTreeNode | None:
204
+ section = sections[-1]
205
+
206
+ section.original_tokens.extend(node.to_tokens())
207
+
208
+ current: SyntaxTreeNode | None = node
209
+
210
+ while current is not None and current.type != "root" and current.next_sibling is None:
211
+ current = current.parent
212
+
213
+ if current is None:
214
+ break
215
+
216
+ if current.type != "root":
217
+ assert current.nester_tokens is not None
218
+ section.original_tokens.append(current.nester_tokens.closing)
219
+
220
+ return current
221
+
222
+
223
+ @unwrap_to_error
224
+ def parse( # noqa: CCR001, CFQ001
225
+ text: str, *, artifact_id: FullArtifactId | None = None
226
+ ) -> Result[list[SectionSource], ErrorsList]: # pylint: disable=R0912, R0915
227
+ md = MarkdownIt("commonmark") # TODO: later we may want to customize it with plugins
228
+
229
+ tokens = md.parse(text)
230
+
231
+ # we do not need root node
232
+ node: SyntaxTreeNode | None = SyntaxTreeNode(tokens).children[0]
233
+
234
+ sections: list[SectionSource] = [
235
+ SectionSource(
236
+ level=SectionLevel.h1,
237
+ title=None,
238
+ original_tokens=[],
239
+ analysis_tokens=[],
240
+ configs=[],
241
+ )
242
+ ]
243
+
244
+ while node is not None:
245
+
246
+ if node.type == "heading":
247
+ node = _parse_heading(sections, node, artifact_id).unwrap()
248
+ continue
249
+
250
+ if node.type == "fence":
251
+ node = _parse_fence(sections, node)
252
+ continue
253
+
254
+ if node.is_nested:
255
+ node = _parse_nested(sections, node)
256
+ continue
257
+
258
+ node = _parse_others(sections, node)
259
+
260
+ if node is None:
261
+ break
262
+
263
+ node = node.next_sibling
264
+
265
+ return Ok(sections)
266
+
267
+ return sections
@@ -0,0 +1 @@
1
+ """Helpers for constructing artifacts from different source types."""
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ import pydantic
7
+
8
+ from donna.core.entities import BaseEntity
9
+ from donna.core.errors import ErrorsList
10
+ from donna.core.result import Result
11
+ from donna.machine.primitives import Primitive
12
+
13
+ if TYPE_CHECKING:
14
+ from donna.domain.ids import FullArtifactId
15
+ from donna.machine.artifacts import Artifact
16
+ from donna.world.artifacts import ArtifactRenderContext
17
+ from donna.world.config import SourceConfig as SourceConfigModel
18
+
19
+
20
+ class SourceConfig(BaseEntity, ABC):
21
+ kind: str
22
+ supported_extensions: list[str] = pydantic.Field(default_factory=list)
23
+
24
+ @classmethod
25
+ def normalize_extension(cls, extension: str) -> str:
26
+ normalized = str(extension).strip().lower()
27
+
28
+ if not normalized:
29
+ raise ValueError("Extension must not be empty")
30
+
31
+ if not normalized.startswith("."):
32
+ normalized = f".{normalized}"
33
+
34
+ if normalized == ".":
35
+ raise ValueError("Extension must include characters after '.'")
36
+
37
+ return normalized
38
+
39
+ @pydantic.field_validator("supported_extensions")
40
+ @classmethod
41
+ def _normalize_supported_extensions(cls, values: list[str]) -> list[str]:
42
+ normalized: list[str] = []
43
+
44
+ for value in values:
45
+ extension = cls.normalize_extension(value)
46
+ if extension not in normalized:
47
+ normalized.append(extension)
48
+
49
+ return normalized
50
+
51
+ def supports_extension(self, extension: str) -> bool:
52
+ return self.normalize_extension(extension) in self.supported_extensions
53
+
54
+ @abstractmethod
55
+ def construct_artifact_from_bytes( # noqa: E704
56
+ self, full_id: "FullArtifactId", content: bytes, render_context: "ArtifactRenderContext"
57
+ ) -> Result["Artifact", ErrorsList]: ... # noqa: E704
58
+
59
+
60
+ class SourceConstructor(Primitive, ABC):
61
+ @abstractmethod
62
+ def construct_source(self, config: SourceConfigModel) -> SourceConfig: ... # noqa: E704
@@ -0,0 +1,260 @@
1
+ import uuid
2
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, cast
3
+
4
+ from donna.core.errors import ErrorsList
5
+ from donna.core.result import Err, Ok, Result, unwrap_to_error
6
+ from donna.domain.ids import ArtifactSectionId, FullArtifactId, PythonImportPath
7
+ from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionConfig, ArtifactSectionMeta
8
+ from donna.machine.primitives import Primitive, resolve_primitive
9
+ from donna.world import errors as world_errors
10
+ from donna.world import markdown
11
+ from donna.world.artifacts import ArtifactRenderContext
12
+ from donna.world.sources.base import SourceConfig, SourceConstructor
13
+ from donna.world.templates import RenderMode, render
14
+
15
+
16
+ class MarkdownSectionConstructor(Protocol):
17
+ def markdown_construct_section(
18
+ self,
19
+ artifact_id: FullArtifactId,
20
+ source: markdown.SectionSource,
21
+ config: dict[str, Any],
22
+ primary: bool = False,
23
+ ) -> Result[ArtifactSection, ErrorsList]:
24
+ pass
25
+
26
+
27
+ class Config(SourceConfig):
28
+ kind: Literal["markdown"] = "markdown"
29
+ supported_extensions: list[str] = [".md", ".markdown"]
30
+ default_section_kind: PythonImportPath = PythonImportPath("donna.lib.text")
31
+ default_primary_section_id: ArtifactSectionId = ArtifactSectionId("primary")
32
+
33
+ def construct_artifact_from_bytes(
34
+ self, full_id: FullArtifactId, content: bytes, render_context: ArtifactRenderContext
35
+ ) -> Result[Artifact, ErrorsList]:
36
+ return construct_artifact_from_bytes(full_id, content, render_context, self)
37
+
38
+
39
+ class MarkdownSourceConstructor(SourceConstructor):
40
+ def construct_source(self, config: "SourceConfigModel") -> Config:
41
+ data = config.model_dump()
42
+ data.pop("kind", None)
43
+ return Config.model_validate(data)
44
+
45
+
46
+ class MarkdownSectionMixin:
47
+ config_class: ClassVar[type[ArtifactSectionConfig]]
48
+
49
+ def markdown_build_title(
50
+ self,
51
+ artifact_id: FullArtifactId,
52
+ source: markdown.SectionSource,
53
+ section_config: ArtifactSectionConfig,
54
+ primary: bool = False,
55
+ ) -> str:
56
+ return source.title or ""
57
+
58
+ def markdown_build_description(
59
+ self,
60
+ artifact_id: FullArtifactId,
61
+ source: markdown.SectionSource,
62
+ section_config: ArtifactSectionConfig,
63
+ primary: bool = False,
64
+ ) -> str:
65
+ return source.as_original_markdown(with_title=False)
66
+
67
+ def markdown_construct_meta(
68
+ self,
69
+ artifact_id: FullArtifactId,
70
+ source: markdown.SectionSource,
71
+ section_config: ArtifactSectionConfig,
72
+ description: str,
73
+ primary: bool = False,
74
+ ) -> Result[ArtifactSectionMeta, ErrorsList]:
75
+ return Ok(ArtifactSectionMeta())
76
+
77
+ @unwrap_to_error
78
+ def markdown_construct_section( # noqa: CCR001
79
+ self,
80
+ artifact_id: FullArtifactId,
81
+ source: markdown.SectionSource,
82
+ config: dict[str, Any],
83
+ primary: bool = False,
84
+ ) -> Result[ArtifactSection, ErrorsList]:
85
+ section_config = self.config_class.parse_obj(config)
86
+
87
+ title = self.markdown_build_title(
88
+ artifact_id=artifact_id,
89
+ source=source,
90
+ section_config=section_config,
91
+ primary=primary,
92
+ )
93
+ description = self.markdown_build_description(
94
+ artifact_id=artifact_id,
95
+ source=source,
96
+ section_config=section_config,
97
+ primary=primary,
98
+ )
99
+ meta = self.markdown_construct_meta(
100
+ artifact_id=artifact_id,
101
+ source=source,
102
+ section_config=section_config,
103
+ description=description,
104
+ primary=primary,
105
+ ).unwrap()
106
+
107
+ return Ok(
108
+ ArtifactSection(
109
+ id=section_config.id,
110
+ artifact_id=artifact_id,
111
+ kind=section_config.kind,
112
+ title=title,
113
+ description=description,
114
+ primary=primary,
115
+ meta=meta,
116
+ )
117
+ )
118
+
119
+
120
+ @unwrap_to_error
121
+ def parse_artifact_content(
122
+ full_id: FullArtifactId, text: str, render_context: ArtifactRenderContext
123
+ ) -> Result[list[markdown.SectionSource], ErrorsList]:
124
+ # Parsing an artifact two times is not ideal, but it is straightforward approach that works for now.
125
+ # We should consider optimizing this in the future if performance or stability becomes an issue.
126
+ # For now let's wait till we have more artifact analysis logic and till more use cases emerge.
127
+
128
+ original_markdown_source = render(full_id, text, render_context).unwrap()
129
+ original_sections = markdown.parse(original_markdown_source, artifact_id=full_id).unwrap()
130
+
131
+ analysis_context = render_context.replace(primary_mode=RenderMode.analysis)
132
+ analyzed_markdown_source = render(full_id, text, analysis_context).unwrap()
133
+ analyzed_sections = markdown.parse(analyzed_markdown_source, artifact_id=full_id).unwrap()
134
+
135
+ if len(original_sections) != len(analyzed_sections):
136
+ raise world_errors.MarkdownSectionsCountMismatch(
137
+ artifact_id=full_id,
138
+ original_count=len(original_sections),
139
+ analyzed_count=len(analyzed_sections),
140
+ )
141
+
142
+ if not original_sections:
143
+ # return Envrironment errors
144
+ return Err([world_errors.MarkdownArtifactWithoutSections(artifact_id=full_id)])
145
+
146
+ for original, analyzed in zip(original_sections, analyzed_sections):
147
+ original.analysis_tokens.extend(analyzed.original_tokens)
148
+
149
+ return Ok(original_sections)
150
+
151
+
152
+ def construct_artifact_from_bytes(
153
+ full_id: FullArtifactId, content: bytes, render_context: ArtifactRenderContext, config: Config
154
+ ) -> Result[Artifact, ErrorsList]:
155
+ return construct_artifact_from_markdown_source(full_id, content.decode("utf-8"), render_context, config)
156
+
157
+
158
+ @unwrap_to_error
159
+ def construct_artifact_from_markdown_source( # noqa: CCR001
160
+ full_id: FullArtifactId, content: str, render_context: ArtifactRenderContext, config: Config
161
+ ) -> Result[Artifact, ErrorsList]:
162
+ original_sections = parse_artifact_content(full_id, content, render_context).unwrap()
163
+ head_config = dict(original_sections[0].merged_configs().unwrap())
164
+ head_kind_value = head_config["kind"]
165
+ if isinstance(head_kind_value, PythonImportPath):
166
+ head_kind = head_kind_value
167
+ else:
168
+ head_kind = PythonImportPath.parse(head_kind_value).unwrap()
169
+
170
+ if "id" not in head_config or head_config["id"] is None:
171
+ head_config["id"] = config.default_primary_section_id
172
+
173
+ primary_primitive = resolve_primitive(head_kind).unwrap()
174
+ _ensure_markdown_constructible(primary_primitive, head_kind).unwrap()
175
+ markdown_primary_primitive = cast(MarkdownSectionMixin, primary_primitive)
176
+
177
+ primary_section_result = markdown_primary_primitive.markdown_construct_section(
178
+ artifact_id=full_id,
179
+ source=original_sections[0],
180
+ config=head_config,
181
+ primary=True,
182
+ )
183
+ if primary_section_result.is_err():
184
+ return Err(primary_section_result.unwrap_err())
185
+ primary_section = primary_section_result.unwrap()
186
+
187
+ sections = construct_sections_from_markdown(
188
+ artifact_id=full_id,
189
+ sections=original_sections[1:],
190
+ default_section_kind=config.default_section_kind,
191
+ ).unwrap()
192
+ sections = [primary_section, *sections]
193
+ return Ok(Artifact(id=full_id, sections=sections))
194
+
195
+
196
+ @unwrap_to_error
197
+ def construct_sections_from_markdown( # noqa: CCR001
198
+ artifact_id: FullArtifactId,
199
+ sections: list[markdown.SectionSource],
200
+ default_section_kind: PythonImportPath,
201
+ primitive_overrides: dict[PythonImportPath, Primitive] | None = None,
202
+ ) -> Result[list[ArtifactSection], ErrorsList]:
203
+ constructed: list[ArtifactSection] = []
204
+ errors: ErrorsList = []
205
+
206
+ for section in sections:
207
+ data = dict(section.merged_configs().unwrap())
208
+
209
+ if "id" not in data or data["id"] is None:
210
+ data["id"] = ArtifactSectionId("markdown" + uuid.uuid4().hex.replace("-", ""))
211
+
212
+ if "kind" not in data:
213
+ data["kind"] = default_section_kind
214
+
215
+ kind_value = data["kind"]
216
+ if isinstance(kind_value, str):
217
+ primitive_id = PythonImportPath.parse(kind_value).unwrap()
218
+ else:
219
+ primitive_id = kind_value
220
+
221
+ primitive = _resolve_primitive(primitive_id, primitive_overrides).unwrap()
222
+ _ensure_markdown_constructible(primitive, primitive_id).unwrap()
223
+ markdown_primitive = cast(MarkdownSectionMixin, primitive)
224
+
225
+ section_result = markdown_primitive.markdown_construct_section(artifact_id, section, data, primary=False)
226
+ if section_result.is_err():
227
+ errors.extend(section_result.unwrap_err())
228
+ continue
229
+ constructed.append(section_result.unwrap())
230
+
231
+ if errors:
232
+ return Err(errors)
233
+
234
+ return Ok(constructed)
235
+
236
+
237
+ def _resolve_primitive(
238
+ primitive_id: PythonImportPath,
239
+ primitive_overrides: dict[PythonImportPath, Primitive] | None = None,
240
+ ) -> Result[Primitive, ErrorsList]:
241
+ if primitive_overrides is not None and primitive_id in primitive_overrides:
242
+ return Ok(primitive_overrides[primitive_id])
243
+
244
+ return resolve_primitive(primitive_id)
245
+
246
+
247
+ def _ensure_markdown_constructible(
248
+ primitive: Primitive,
249
+ primitive_id: PythonImportPath | str | None = None,
250
+ ) -> Result[None, ErrorsList]:
251
+ if isinstance(primitive, MarkdownSectionMixin):
252
+ return Ok(None)
253
+
254
+ kind_label = f"'{primitive_id}'" if primitive_id is not None else repr(primitive)
255
+
256
+ return Err([world_errors.PrimitiveDoesNotSupportMarkdown(primitive_id=kind_label)])
257
+
258
+
259
+ if TYPE_CHECKING:
260
+ from donna.world.config import SourceConfig as SourceConfigModel