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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess # noqa: S404
|
|
3
|
+
import tempfile
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar, Iterator, cast
|
|
5
|
+
|
|
6
|
+
import pydantic
|
|
7
|
+
|
|
8
|
+
from donna.core import errors as core_errors
|
|
9
|
+
from donna.core.errors import ErrorsList
|
|
10
|
+
from donna.core.result import Err, Ok, Result
|
|
11
|
+
from donna.domain.ids import ArtifactSectionId, FullArtifactId
|
|
12
|
+
from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionConfig, ArtifactSectionMeta
|
|
13
|
+
from donna.machine.errors import ArtifactValidationError
|
|
14
|
+
from donna.machine.operations import OperationConfig, OperationKind, OperationMeta
|
|
15
|
+
from donna.world import markdown
|
|
16
|
+
from donna.world.sources.markdown import MarkdownSectionMixin
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from donna.machine.changes import Change
|
|
20
|
+
from donna.machine.tasks import Task, WorkUnit
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class InternalError(core_errors.InternalError):
|
|
24
|
+
"""Base class for internal errors in donna.primitives.operations.run_script."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RunScriptMissingGotoOnSuccess(ArtifactValidationError):
|
|
28
|
+
code: str = "donna.workflows.run_script_missing_goto_on_success"
|
|
29
|
+
message: str = "Run script operation `{error.section_id}` must define `goto_on_success`."
|
|
30
|
+
ways_to_fix: list[str] = [
|
|
31
|
+
'Add `goto_on_success = "<next_operation_id>"` to the operation config block.',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RunScriptMissingGotoOnFailure(ArtifactValidationError):
|
|
36
|
+
code: str = "donna.workflows.run_script_missing_goto_on_failure"
|
|
37
|
+
message: str = "Run script operation `{error.section_id}` must define `goto_on_failure`."
|
|
38
|
+
ways_to_fix: list[str] = [
|
|
39
|
+
'Add `goto_on_failure = "<next_operation_id>"` to the operation config block.',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RunScriptMissingScriptBlock(ArtifactValidationError):
|
|
44
|
+
code: str = "donna.workflows.run_script_missing_script_block"
|
|
45
|
+
message: str = "Run script operation `{error.section_id}` must include a single `donna script` code block."
|
|
46
|
+
ways_to_fix: list[str] = [
|
|
47
|
+
"Add exactly one fenced code block starting with ` ``` donna script ` in the operation body.",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RunScriptMultipleScriptBlocks(ArtifactValidationError):
|
|
52
|
+
code: str = "donna.workflows.run_script_multiple_script_blocks"
|
|
53
|
+
message: str = "Run script operation `{error.section_id}` must include exactly one `donna script` code block."
|
|
54
|
+
ways_to_fix: list[str] = [
|
|
55
|
+
"Remove extra `donna script` code blocks so only one remains in the operation body.",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RunScriptGotoOnCodeIncludesZero(ArtifactValidationError):
|
|
60
|
+
code: str = "donna.workflows.run_script_goto_on_code_includes_zero"
|
|
61
|
+
message: str = "Run script operation `{error.section_id}` must not map exit code 0 in `goto_on_code`."
|
|
62
|
+
ways_to_fix: list[str] = [
|
|
63
|
+
"Remove the `0` entry from `goto_on_code` and use `goto_on_success` instead.",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RunScriptInvalidExitCode(ArtifactValidationError):
|
|
68
|
+
code: str = "donna.workflows.run_script_invalid_exit_code"
|
|
69
|
+
message: str = "Run script operation `{error.section_id}` has invalid exit code `{error.exit_code}`."
|
|
70
|
+
ways_to_fix: list[str] = [
|
|
71
|
+
'Use integer exit code keys in `goto_on_code` (e.g., `"1" = "next_op"`).',
|
|
72
|
+
]
|
|
73
|
+
exit_code: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RunScriptConfig(OperationConfig):
|
|
77
|
+
save_stdout_to: str | None = None
|
|
78
|
+
save_stderr_to: str | None = None
|
|
79
|
+
goto_on_success: ArtifactSectionId | None = None
|
|
80
|
+
goto_on_failure: ArtifactSectionId | None = None
|
|
81
|
+
goto_on_code: dict[str, ArtifactSectionId] = pydantic.Field(default_factory=dict)
|
|
82
|
+
timeout: int = 60
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RunScriptMeta(OperationMeta):
|
|
86
|
+
script: str | None = None
|
|
87
|
+
save_stdout_to: str | None = None
|
|
88
|
+
save_stderr_to: str | None = None
|
|
89
|
+
goto_on_success: ArtifactSectionId | None = None
|
|
90
|
+
goto_on_failure: ArtifactSectionId | None = None
|
|
91
|
+
goto_on_code: dict[str, ArtifactSectionId] = pydantic.Field(default_factory=dict)
|
|
92
|
+
timeout: int = 60
|
|
93
|
+
|
|
94
|
+
def select_next_operation(self, exit_code: int) -> ArtifactSectionId:
|
|
95
|
+
if exit_code == 0:
|
|
96
|
+
next_operation = self.goto_on_success
|
|
97
|
+
else:
|
|
98
|
+
next_operation = self.goto_on_code.get(str(exit_code))
|
|
99
|
+
if next_operation is None:
|
|
100
|
+
next_operation = self.goto_on_failure
|
|
101
|
+
|
|
102
|
+
assert next_operation is not None
|
|
103
|
+
return next_operation
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RunScript(MarkdownSectionMixin, OperationKind):
|
|
107
|
+
config_class: ClassVar[type[RunScriptConfig]] = RunScriptConfig
|
|
108
|
+
|
|
109
|
+
def markdown_construct_meta(
|
|
110
|
+
self,
|
|
111
|
+
artifact_id: "FullArtifactId",
|
|
112
|
+
source: markdown.SectionSource,
|
|
113
|
+
section_config: ArtifactSectionConfig,
|
|
114
|
+
description: str,
|
|
115
|
+
primary: bool = False,
|
|
116
|
+
) -> Result[ArtifactSectionMeta, ErrorsList]:
|
|
117
|
+
run_config = cast(RunScriptConfig, section_config)
|
|
118
|
+
scripts = source.scripts()
|
|
119
|
+
if not scripts:
|
|
120
|
+
return Err([RunScriptMissingScriptBlock(artifact_id=artifact_id, section_id=run_config.id)])
|
|
121
|
+
if len(scripts) > 1:
|
|
122
|
+
return Err([RunScriptMultipleScriptBlocks(artifact_id=artifact_id, section_id=run_config.id)])
|
|
123
|
+
|
|
124
|
+
script = scripts[0]
|
|
125
|
+
allowed_transitions: set[ArtifactSectionId] = set()
|
|
126
|
+
|
|
127
|
+
if run_config.goto_on_success is not None:
|
|
128
|
+
allowed_transitions.add(run_config.goto_on_success)
|
|
129
|
+
|
|
130
|
+
if run_config.goto_on_failure is not None:
|
|
131
|
+
allowed_transitions.add(run_config.goto_on_failure)
|
|
132
|
+
|
|
133
|
+
if run_config.goto_on_code:
|
|
134
|
+
allowed_transitions.update(run_config.goto_on_code.values())
|
|
135
|
+
|
|
136
|
+
return Ok(
|
|
137
|
+
RunScriptMeta(
|
|
138
|
+
fsm_mode=run_config.fsm_mode,
|
|
139
|
+
allowed_transtions=allowed_transitions,
|
|
140
|
+
script=script,
|
|
141
|
+
save_stdout_to=run_config.save_stdout_to,
|
|
142
|
+
save_stderr_to=run_config.save_stderr_to,
|
|
143
|
+
goto_on_success=run_config.goto_on_success,
|
|
144
|
+
goto_on_failure=run_config.goto_on_failure,
|
|
145
|
+
goto_on_code=dict(run_config.goto_on_code),
|
|
146
|
+
timeout=run_config.timeout,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def execute_section(self, task: "Task", unit: "WorkUnit", operation: ArtifactSection) -> Iterator["Change"]:
|
|
151
|
+
from donna.machine.changes import ChangeAddWorkUnit, ChangeSetTaskContext
|
|
152
|
+
|
|
153
|
+
meta = cast(RunScriptMeta, operation.meta)
|
|
154
|
+
|
|
155
|
+
script = meta.script
|
|
156
|
+
assert script is not None
|
|
157
|
+
|
|
158
|
+
stdout, stderr, exit_code = _run_script(script, meta.timeout)
|
|
159
|
+
|
|
160
|
+
if meta.save_stdout_to is not None:
|
|
161
|
+
yield ChangeSetTaskContext(task_id=task.id, key=meta.save_stdout_to, value=stdout)
|
|
162
|
+
|
|
163
|
+
if meta.save_stderr_to is not None:
|
|
164
|
+
yield ChangeSetTaskContext(task_id=task.id, key=meta.save_stderr_to, value=stderr)
|
|
165
|
+
|
|
166
|
+
next_operation = meta.select_next_operation(exit_code)
|
|
167
|
+
full_operation_id = unit.operation_id.full_artifact_id.to_full_local(next_operation)
|
|
168
|
+
|
|
169
|
+
yield ChangeAddWorkUnit(task_id=task.id, operation_id=full_operation_id)
|
|
170
|
+
|
|
171
|
+
def validate_section( # noqa: CCR001
|
|
172
|
+
self, artifact: Artifact, section_id: ArtifactSectionId
|
|
173
|
+
) -> Result[None, ErrorsList]:
|
|
174
|
+
section = artifact.get_section(section_id).unwrap()
|
|
175
|
+
|
|
176
|
+
meta = cast(RunScriptMeta, section.meta)
|
|
177
|
+
|
|
178
|
+
errors: ErrorsList = []
|
|
179
|
+
|
|
180
|
+
if meta.goto_on_success is None:
|
|
181
|
+
errors.append(RunScriptMissingGotoOnSuccess(artifact_id=artifact.id, section_id=section_id))
|
|
182
|
+
|
|
183
|
+
if meta.goto_on_failure is None:
|
|
184
|
+
errors.append(RunScriptMissingGotoOnFailure(artifact_id=artifact.id, section_id=section_id))
|
|
185
|
+
|
|
186
|
+
for code in meta.goto_on_code:
|
|
187
|
+
try:
|
|
188
|
+
parsed = int(code)
|
|
189
|
+
except ValueError:
|
|
190
|
+
errors.append(
|
|
191
|
+
RunScriptInvalidExitCode(
|
|
192
|
+
artifact_id=artifact.id,
|
|
193
|
+
section_id=section_id,
|
|
194
|
+
exit_code=code,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
if parsed == 0:
|
|
200
|
+
errors.append(RunScriptGotoOnCodeIncludesZero(artifact_id=artifact.id, section_id=section_id))
|
|
201
|
+
|
|
202
|
+
if errors:
|
|
203
|
+
return Err(errors)
|
|
204
|
+
|
|
205
|
+
return Ok(None)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _run_script(script: str, timeout: int) -> tuple[str, str, int]: # noqa: CCR001
|
|
209
|
+
temp_path = None
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
with tempfile.NamedTemporaryFile("w", prefix="donna-script-", delete=False) as temp:
|
|
213
|
+
temp.write(script)
|
|
214
|
+
temp.flush()
|
|
215
|
+
temp_path = temp.name
|
|
216
|
+
|
|
217
|
+
os.chmod(temp_path, 0o700)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
result = subprocess.run( # noqa: S603
|
|
221
|
+
[temp_path],
|
|
222
|
+
capture_output=True,
|
|
223
|
+
text=True,
|
|
224
|
+
env=os.environ.copy(),
|
|
225
|
+
stdin=subprocess.DEVNULL,
|
|
226
|
+
timeout=timeout,
|
|
227
|
+
check=False,
|
|
228
|
+
)
|
|
229
|
+
except subprocess.TimeoutExpired as exc:
|
|
230
|
+
stdout = _coerce_output(exc.stdout)
|
|
231
|
+
stderr = _coerce_output(exc.stderr)
|
|
232
|
+
return stdout, stderr, 124
|
|
233
|
+
|
|
234
|
+
return _coerce_output(result.stdout), _coerce_output(result.stderr), result.returncode
|
|
235
|
+
finally:
|
|
236
|
+
if temp_path is not None:
|
|
237
|
+
try:
|
|
238
|
+
os.remove(temp_path)
|
|
239
|
+
except FileNotFoundError:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _coerce_output(value: str | bytes | None) -> str:
|
|
244
|
+
if value is None:
|
|
245
|
+
return ""
|
|
246
|
+
|
|
247
|
+
if isinstance(value, bytes):
|
|
248
|
+
return value.decode("utf-8", errors="replace")
|
|
249
|
+
|
|
250
|
+
return value
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from donna.protocol.cells import Cell, MetaValue
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def operation_succeeded(message: str, **meta: MetaValue) -> Cell:
|
|
5
|
+
return Cell.build(kind="operation_succeeded", media_type="text/markdown", content=message, **meta)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def operation_failed(message: str, **meta: MetaValue) -> Cell:
|
|
9
|
+
return Cell.build(kind="operation_failed", media_type="text/markdown", content=message, **meta)
|
donna/protocol/cells.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
import pydantic
|
|
5
|
+
|
|
6
|
+
from donna.core.entities import BaseEntity
|
|
7
|
+
|
|
8
|
+
MetaValue = str | int | bool | None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def to_meta_value(value: object) -> MetaValue:
|
|
12
|
+
if isinstance(value, (str, int, bool)) or value is None:
|
|
13
|
+
return value
|
|
14
|
+
|
|
15
|
+
return str(value)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Cell(BaseEntity):
|
|
19
|
+
id: uuid.UUID = pydantic.Field(default_factory=uuid.uuid4)
|
|
20
|
+
kind: str
|
|
21
|
+
media_type: str | None
|
|
22
|
+
content: str | None
|
|
23
|
+
meta: dict[str, MetaValue] = pydantic.Field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def build(cls, kind: str, media_type: str | None, content: str | None, **meta: MetaValue) -> "Cell":
|
|
27
|
+
if media_type is None and content is not None:
|
|
28
|
+
from donna.protocol.errors import ContentWithoutMediaType
|
|
29
|
+
|
|
30
|
+
raise ContentWithoutMediaType()
|
|
31
|
+
|
|
32
|
+
return cls(kind=kind, media_type=media_type, content=content, meta=meta)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def build_meta(cls, kind: str, **meta: MetaValue) -> "Cell":
|
|
36
|
+
return cls.build(kind=kind, media_type=None, content=None, **meta)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def build_markdown(cls, kind: str, content: str, **meta: MetaValue) -> "Cell":
|
|
40
|
+
return cls.build(kind=kind, media_type="text/markdown", content=content, **meta)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def short_id(self) -> str:
|
|
44
|
+
return base64.urlsafe_b64encode(self.id.bytes).rstrip(b"=").decode()
|
donna/protocol/errors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from donna.core import errors as core_errors
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class InternalError(core_errors.InternalError):
|
|
5
|
+
"""Base class for internal errors in donna.protocol."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModeNotSet(InternalError):
|
|
9
|
+
message: str = "Mode is not set. Pass -p <mode> to the CLI."
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnsupportedFormatterMode(InternalError):
|
|
13
|
+
message: str = "Formatter for mode '{mode}' is not implemented."
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContentWithoutMediaType(InternalError):
|
|
17
|
+
message: str = "Cannot set content when media_type is None."
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from donna.protocol.cells import Cell
|
|
4
|
+
from donna.protocol.formatters.base import Formatter as BaseFormatter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Formatter(BaseFormatter):
|
|
8
|
+
|
|
9
|
+
def format_cell(self, cell: Cell, single_mode: bool) -> bytes:
|
|
10
|
+
data: dict[str, str | int | bool | None] = {"id": cell.short_id}
|
|
11
|
+
|
|
12
|
+
for meta_key, meta_value in sorted(cell.meta.items()):
|
|
13
|
+
data[meta_key] = meta_value
|
|
14
|
+
|
|
15
|
+
data["content"] = cell.content.strip() if cell.content else None
|
|
16
|
+
|
|
17
|
+
return json.dumps(data, ensure_ascii=False, indent=None, separators=(",", ":"), sort_keys=True).encode()
|
|
18
|
+
|
|
19
|
+
def format_log(self, cell: Cell, single_mode: bool) -> bytes:
|
|
20
|
+
return self.format_cells([cell])
|
|
21
|
+
|
|
22
|
+
def format_cells(self, cells: list[Cell]) -> bytes:
|
|
23
|
+
single_mode = len(cells) == 1
|
|
24
|
+
formatted_cells = [self.format_cell(cell, single_mode=single_mode) for cell in cells]
|
|
25
|
+
return b"\n".join(formatted_cells)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from donna.protocol.cells import Cell
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Formatter(ABC):
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def format_cell(self, cell: Cell, single_mode: bool) -> bytes: ... # noqa: E704
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def format_log(self, cell: Cell, single_mode: bool) -> bytes: ... # noqa: E704
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def format_cells(self, cells: list[Cell]) -> bytes: ... # noqa: E704
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from donna.protocol.cells import Cell
|
|
2
|
+
from donna.protocol.formatters.base import Formatter as BaseFormatter
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Formatter(BaseFormatter):
|
|
6
|
+
|
|
7
|
+
def format_cell(self, cell: Cell, single_mode: bool) -> bytes:
|
|
8
|
+
id = cell.short_id
|
|
9
|
+
|
|
10
|
+
lines = []
|
|
11
|
+
|
|
12
|
+
if not single_mode:
|
|
13
|
+
lines = [f"----- DONNA CELL {id} -----"]
|
|
14
|
+
|
|
15
|
+
lines.append(f"kind = {cell.kind}")
|
|
16
|
+
|
|
17
|
+
if cell.media_type is not None:
|
|
18
|
+
lines.append(f"media_type = {cell.media_type}")
|
|
19
|
+
|
|
20
|
+
for meta_key, meta_value in sorted(cell.meta.items()):
|
|
21
|
+
lines.append(f"{meta_key} = {meta_value}")
|
|
22
|
+
|
|
23
|
+
if cell.content:
|
|
24
|
+
lines.append("")
|
|
25
|
+
lines.append(cell.content.strip())
|
|
26
|
+
|
|
27
|
+
return "\n".join(lines).encode()
|
|
28
|
+
|
|
29
|
+
def format_log(self, cell: Cell, single_mode: bool) -> bytes:
|
|
30
|
+
message = cell.content.strip() if cell.content else ""
|
|
31
|
+
return f"DONNA LOG: {message}".strip().encode()
|
|
32
|
+
|
|
33
|
+
def format_cells(self, cells: list[Cell]) -> bytes:
|
|
34
|
+
single_mode = len(cells) == 1
|
|
35
|
+
formatted_cells = [self.format_cell(cell, single_mode=single_mode) for cell in cells]
|
|
36
|
+
return b"\n\n".join(formatted_cells)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from donna.protocol.cells import Cell
|
|
2
|
+
from donna.protocol.formatters.base import Formatter as BaseFormatter
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Formatter(BaseFormatter):
|
|
6
|
+
|
|
7
|
+
def format_cell(self, cell: Cell, single_mode: bool) -> bytes: # noqa: CCR001
|
|
8
|
+
id = cell.short_id
|
|
9
|
+
|
|
10
|
+
lines = []
|
|
11
|
+
|
|
12
|
+
if not single_mode:
|
|
13
|
+
lines = [f"--DONNA-CELL {id} BEGIN--"]
|
|
14
|
+
|
|
15
|
+
lines.append(f"kind={cell.kind}")
|
|
16
|
+
|
|
17
|
+
if cell.media_type is not None:
|
|
18
|
+
lines.append(f"media_type={cell.media_type}")
|
|
19
|
+
|
|
20
|
+
for meta_key, meta_value in sorted(cell.meta.items()):
|
|
21
|
+
lines.append(f"{meta_key}={meta_value}")
|
|
22
|
+
|
|
23
|
+
if cell.content:
|
|
24
|
+
lines.append("")
|
|
25
|
+
lines.append(cell.content.strip())
|
|
26
|
+
|
|
27
|
+
if not single_mode:
|
|
28
|
+
lines.append(f"--DONNA-CELL {id} END--")
|
|
29
|
+
|
|
30
|
+
return "\n".join(lines).strip().encode()
|
|
31
|
+
|
|
32
|
+
def format_log(self, cell: Cell, single_mode: bool) -> bytes:
|
|
33
|
+
message = cell.content.strip() if cell.content else ""
|
|
34
|
+
return f"DONNA LOG: {message}".strip().encode()
|
|
35
|
+
|
|
36
|
+
def format_cells(self, cells: list[Cell]) -> bytes:
|
|
37
|
+
single_mode = len(cells) == 1
|
|
38
|
+
formatted_cells = [self.format_cell(cell, single_mode=single_mode) for cell in cells]
|
|
39
|
+
return b"\n\n".join(formatted_cells)
|
donna/protocol/modes.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
from donna.protocol.errors import ModeNotSet, UnsupportedFormatterMode
|
|
4
|
+
from donna.protocol.formatters.automation import Formatter as AutomationFormatter
|
|
5
|
+
from donna.protocol.formatters.base import Formatter
|
|
6
|
+
from donna.protocol.formatters.human import Formatter as HumanFormatter
|
|
7
|
+
from donna.protocol.formatters.llm import Formatter as LLMFormatter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Mode(enum.StrEnum):
|
|
11
|
+
human = "human"
|
|
12
|
+
llm = "llm"
|
|
13
|
+
automation = "automation"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_MODE: Mode | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def set_mode(mode: Mode) -> None:
|
|
20
|
+
global _MODE
|
|
21
|
+
_MODE = mode
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mode() -> Mode:
|
|
25
|
+
if _MODE is None:
|
|
26
|
+
raise ModeNotSet()
|
|
27
|
+
|
|
28
|
+
return _MODE
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_cell_formatter() -> Formatter:
|
|
32
|
+
match mode():
|
|
33
|
+
case Mode.human:
|
|
34
|
+
return HumanFormatter()
|
|
35
|
+
case Mode.llm:
|
|
36
|
+
return LLMFormatter()
|
|
37
|
+
case Mode.automation:
|
|
38
|
+
return AutomationFormatter()
|
|
39
|
+
case _:
|
|
40
|
+
raise UnsupportedFormatterMode(mode=mode())
|
donna/protocol/nodes.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from donna.protocol.cells import Cell
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Node(ABC):
|
|
7
|
+
"""Node of Donna's knowledge graph.
|
|
8
|
+
|
|
9
|
+
The concept of knowledge graph is highly experimental and subject to change.
|
|
10
|
+
|
|
11
|
+
Its primary purpose is to simplify navigation through different Donna's entities
|
|
12
|
+
and to provide a unified interface for retrieving information about them.
|
|
13
|
+
|
|
14
|
+
There are two types of child nodes:
|
|
15
|
+
|
|
16
|
+
- References — nodes that are referenced by this artifact, but not a part of it.
|
|
17
|
+
An artifact does not include information from referenced nodes
|
|
18
|
+
in its info.
|
|
19
|
+
An artifact include references in its details and index.
|
|
20
|
+
- Components — nodes that are inseparable parts of this artifact.
|
|
21
|
+
An artifact includes information from them in its info.
|
|
22
|
+
An artifact does not include components in its details and index,
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__slots__ = ()
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def status(self) -> Cell:
|
|
29
|
+
"""Returns short info about only this node."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def info(self) -> Cell:
|
|
33
|
+
"""Returns full info about only this node."""
|
|
34
|
+
return self.status()
|
|
35
|
+
|
|
36
|
+
def details(self) -> list[Cell]:
|
|
37
|
+
"""Returns info about the node and its children.
|
|
38
|
+
|
|
39
|
+
The node decides itself which children to include with what level of detail.
|
|
40
|
+
"""
|
|
41
|
+
cells = [self.info()]
|
|
42
|
+
cells.extend(child.info() for child in self.references())
|
|
43
|
+
|
|
44
|
+
return cells
|
|
45
|
+
|
|
46
|
+
def index(self) -> list[Cell]:
|
|
47
|
+
"""Returns status of itself and all its children."""
|
|
48
|
+
cells = [self.status()]
|
|
49
|
+
cells.extend(child.status() for child in self.references())
|
|
50
|
+
|
|
51
|
+
return cells
|
|
52
|
+
|
|
53
|
+
def references(self) -> list["Node"]:
|
|
54
|
+
"""Return all nodes that are referenced by this one"""
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
def components(self) -> list["Node"]:
|
|
58
|
+
"""Return all nodes that are iseparable parts of this one."""
|
|
59
|
+
return []
|
donna/world/__init__.py
ADDED
|
File without changes
|
donna/world/artifacts.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
from donna.core.entities import BaseEntity
|
|
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 FullArtifactId, FullArtifactIdPattern, WorldId
|
|
7
|
+
from donna.machine.artifacts import Artifact
|
|
8
|
+
from donna.machine.tasks import Task, WorkUnit
|
|
9
|
+
from donna.world import errors
|
|
10
|
+
from donna.world.config import config
|
|
11
|
+
from donna.world.templates import RenderMode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ArtifactRenderContext(BaseEntity):
|
|
15
|
+
primary_mode: RenderMode
|
|
16
|
+
current_task: Task | None = None
|
|
17
|
+
current_work_unit: WorkUnit | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ArtifactUpdateError(errors.WorldError):
|
|
21
|
+
cell_kind: str = "artifact_update_error"
|
|
22
|
+
artifact_id: FullArtifactId
|
|
23
|
+
path: pathlib.Path
|
|
24
|
+
|
|
25
|
+
def content_intro(self) -> str:
|
|
26
|
+
return f"Error updating artifact '{self.artifact_id}' from the path '{self.path}'"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CanNotUpdateReadonlyWorld(ArtifactUpdateError):
|
|
30
|
+
code: str = "donna.world.cannot_update_readonly_world"
|
|
31
|
+
message: str = "Cannot upload artifact to the read-only world `{error.world_id}`"
|
|
32
|
+
world_id: WorldId
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InputPathHasNoExtension(ArtifactUpdateError):
|
|
36
|
+
code: str = "donna.world.input_path_has_no_extension"
|
|
37
|
+
message: str = "Input path has no extension to determine artifact source type"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NoSourceForArtifactExtension(ArtifactUpdateError):
|
|
41
|
+
code: str = "donna.world.no_source_for_artifact_extension"
|
|
42
|
+
message: str = "No source found for artifact extension of input path"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@unwrap_to_error
|
|
46
|
+
def artifact_file_extension(full_id: FullArtifactId) -> Result[str, ErrorsList]:
|
|
47
|
+
world = config().get_world(full_id.world_id).unwrap()
|
|
48
|
+
return Ok(world.file_extension_for(full_id.artifact_id).unwrap().lstrip("."))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@unwrap_to_error
|
|
52
|
+
def fetch_artifact(full_id: FullArtifactId, output: pathlib.Path) -> Result[None, ErrorsList]:
|
|
53
|
+
world = config().get_world(full_id.world_id).unwrap()
|
|
54
|
+
content = world.fetch_source(full_id.artifact_id).unwrap()
|
|
55
|
+
|
|
56
|
+
with output.open("wb") as f:
|
|
57
|
+
f.write(content)
|
|
58
|
+
|
|
59
|
+
return Ok(None)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@unwrap_to_error
|
|
63
|
+
def update_artifact(full_id: FullArtifactId, input: pathlib.Path) -> Result[None, ErrorsList]:
|
|
64
|
+
world = config().get_world(full_id.world_id).unwrap()
|
|
65
|
+
|
|
66
|
+
if world.readonly:
|
|
67
|
+
return Err([CanNotUpdateReadonlyWorld(artifact_id=full_id, path=input, world_id=world.id)])
|
|
68
|
+
|
|
69
|
+
source_suffix = input.suffix.lower()
|
|
70
|
+
content_bytes = input.read_bytes()
|
|
71
|
+
|
|
72
|
+
if not source_suffix:
|
|
73
|
+
return Err([InputPathHasNoExtension(artifact_id=full_id, path=input)])
|
|
74
|
+
|
|
75
|
+
source_config = config().find_source_for_extension(source_suffix)
|
|
76
|
+
if source_config is None:
|
|
77
|
+
return Err([NoSourceForArtifactExtension(artifact_id=full_id, path=input)])
|
|
78
|
+
|
|
79
|
+
render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
|
|
80
|
+
test_artifact = source_config.construct_artifact_from_bytes(full_id, content_bytes, render_context).unwrap()
|
|
81
|
+
validation_result = test_artifact.validate_artifact()
|
|
82
|
+
|
|
83
|
+
validation_result.unwrap()
|
|
84
|
+
world.update(full_id.artifact_id, content_bytes, source_suffix).unwrap()
|
|
85
|
+
|
|
86
|
+
return Ok(None)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@unwrap_to_error
|
|
90
|
+
def load_artifact(
|
|
91
|
+
full_id: FullArtifactId, render_context: ArtifactRenderContext | None = None
|
|
92
|
+
) -> Result[Artifact, ErrorsList]:
|
|
93
|
+
if render_context is None:
|
|
94
|
+
render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
|
|
95
|
+
|
|
96
|
+
world = config().get_world(full_id.world_id).unwrap()
|
|
97
|
+
return Ok(world.fetch(full_id.artifact_id, render_context).unwrap())
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def list_artifacts( # noqa: CCR001
|
|
101
|
+
pattern: FullArtifactIdPattern, render_context: ArtifactRenderContext | None = None
|
|
102
|
+
) -> Result[list[Artifact], ErrorsList]:
|
|
103
|
+
|
|
104
|
+
if render_context is None:
|
|
105
|
+
render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
|
|
106
|
+
|
|
107
|
+
artifacts: list[Artifact] = []
|
|
108
|
+
errors: ErrorsList = []
|
|
109
|
+
|
|
110
|
+
for world in reversed(config().worlds_instances):
|
|
111
|
+
for artifact_id in world.list_artifacts(pattern):
|
|
112
|
+
full_id = FullArtifactId((world.id, artifact_id))
|
|
113
|
+
artifact_result = load_artifact(full_id, render_context)
|
|
114
|
+
if artifact_result.is_err():
|
|
115
|
+
errors.extend(artifact_result.unwrap_err())
|
|
116
|
+
continue
|
|
117
|
+
artifacts.append(artifact_result.unwrap())
|
|
118
|
+
|
|
119
|
+
if errors:
|
|
120
|
+
return Err(errors)
|
|
121
|
+
|
|
122
|
+
return Ok(artifacts)
|