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.
- donna/__init__.py +1 -0
- donna/artifacts/__init__.py +0 -0
- donna/artifacts/usage/__init__.py +0 -0
- donna/artifacts/usage/artifacts.md +224 -0
- donna/artifacts/usage/cli.md +117 -0
- donna/artifacts/usage/worlds.md +36 -0
- donna/artifacts/work/__init__.py +0 -0
- donna/artifacts/work/do_it.md +142 -0
- donna/artifacts/work/do_it_fast.md +98 -0
- donna/artifacts/work/planning.md +245 -0
- donna/cli/__init__.py +0 -0
- donna/cli/__main__.py +6 -0
- donna/cli/application.py +17 -0
- donna/cli/commands/__init__.py +0 -0
- donna/cli/commands/artifacts.py +110 -0
- donna/cli/commands/projects.py +49 -0
- donna/cli/commands/sessions.py +77 -0
- donna/cli/types.py +138 -0
- donna/cli/utils.py +53 -0
- donna/core/__init__.py +0 -0
- donna/core/entities.py +27 -0
- donna/core/errors.py +126 -0
- donna/core/result.py +99 -0
- donna/core/utils.py +37 -0
- donna/domain/__init__.py +0 -0
- donna/domain/errors.py +47 -0
- donna/domain/ids.py +497 -0
- donna/lib/__init__.py +21 -0
- donna/lib/sources.py +5 -0
- donna/lib/worlds.py +7 -0
- donna/machine/__init__.py +0 -0
- donna/machine/action_requests.py +50 -0
- donna/machine/artifacts.py +200 -0
- donna/machine/changes.py +91 -0
- donna/machine/errors.py +122 -0
- donna/machine/operations.py +31 -0
- donna/machine/primitives.py +77 -0
- donna/machine/sessions.py +215 -0
- donna/machine/state.py +244 -0
- donna/machine/tasks.py +89 -0
- donna/machine/templates.py +83 -0
- donna/primitives/__init__.py +1 -0
- donna/primitives/artifacts/__init__.py +0 -0
- donna/primitives/artifacts/specification.py +20 -0
- donna/primitives/artifacts/workflow.py +195 -0
- donna/primitives/directives/__init__.py +0 -0
- donna/primitives/directives/goto.py +44 -0
- donna/primitives/directives/task_variable.py +73 -0
- donna/primitives/directives/view.py +45 -0
- donna/primitives/operations/__init__.py +0 -0
- donna/primitives/operations/finish_workflow.py +37 -0
- donna/primitives/operations/request_action.py +89 -0
- donna/primitives/operations/run_script.py +250 -0
- donna/protocol/__init__.py +0 -0
- donna/protocol/cell_shortcuts.py +9 -0
- donna/protocol/cells.py +44 -0
- donna/protocol/errors.py +17 -0
- donna/protocol/formatters/__init__.py +0 -0
- donna/protocol/formatters/automation.py +25 -0
- donna/protocol/formatters/base.py +15 -0
- donna/protocol/formatters/human.py +36 -0
- donna/protocol/formatters/llm.py +39 -0
- donna/protocol/modes.py +40 -0
- donna/protocol/nodes.py +59 -0
- donna/world/__init__.py +0 -0
- donna/world/artifacts.py +122 -0
- donna/world/artifacts_discovery.py +90 -0
- donna/world/config.py +198 -0
- donna/world/errors.py +232 -0
- donna/world/initialization.py +42 -0
- donna/world/markdown.py +267 -0
- donna/world/sources/__init__.py +1 -0
- donna/world/sources/base.py +62 -0
- donna/world/sources/markdown.py +260 -0
- donna/world/templates.py +181 -0
- donna/world/tmp.py +33 -0
- donna/world/worlds/__init__.py +0 -0
- donna/world/worlds/base.py +68 -0
- donna/world/worlds/filesystem.py +189 -0
- donna/world/worlds/python.py +196 -0
- donna-0.2.0.dist-info/METADATA +44 -0
- donna-0.2.0.dist-info/RECORD +85 -0
- donna-0.2.0.dist-info/WHEEL +4 -0
- donna-0.2.0.dist-info/entry_points.txt +3 -0
- donna-0.2.0.dist-info/licenses/LICENSE +28 -0
donna/world/markdown.py
ADDED
|
@@ -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
|