donna 0.2.0__py3-none-any.whl → 0.2.1__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/artifacts/intro.md +39 -0
- donna/artifacts/research/specs/report.md +163 -0
- donna/artifacts/research/work/research.md +198 -0
- donna/artifacts/rfc/specs/request_for_change.md +271 -0
- donna/artifacts/rfc/work/create.md +120 -0
- donna/artifacts/rfc/work/do.md +109 -0
- donna/artifacts/rfc/work/plan.md +68 -0
- donna/artifacts/usage/artifacts.md +41 -6
- donna/artifacts/usage/cli.md +106 -37
- donna/artifacts/usage/worlds.md +8 -2
- donna/cli/__main__.py +1 -1
- donna/cli/commands/artifacts.py +104 -17
- donna/cli/commands/sessions.py +7 -7
- donna/cli/commands/workspaces.py +42 -0
- donna/cli/errors.py +18 -0
- donna/cli/types.py +16 -9
- donna/cli/utils.py +2 -2
- donna/core/errors.py +1 -11
- donna/core/result.py +5 -8
- donna/core/utils.py +0 -3
- donna/lib/__init__.py +4 -0
- donna/lib/sources.py +1 -1
- donna/lib/worlds.py +2 -2
- donna/machine/action_requests.py +0 -5
- donna/machine/artifacts.py +8 -6
- donna/machine/primitives.py +4 -4
- donna/machine/sessions.py +12 -4
- donna/machine/state.py +2 -2
- donna/machine/tasks.py +4 -18
- donna/machine/templates.py +4 -2
- donna/primitives/artifacts/specification.py +13 -2
- donna/primitives/artifacts/workflow.py +11 -2
- donna/primitives/directives/list.py +86 -0
- donna/primitives/directives/view.py +52 -11
- donna/primitives/operations/finish_workflow.py +13 -2
- donna/primitives/operations/output.py +87 -0
- donna/primitives/operations/request_action.py +3 -9
- donna/primitives/operations/run_script.py +2 -2
- donna/protocol/utils.py +22 -0
- donna/workspaces/artifacts.py +238 -0
- donna/{world → workspaces}/artifacts_discovery.py +1 -1
- donna/{world → workspaces}/config.py +13 -6
- donna/{world → workspaces}/errors.py +55 -45
- donna/workspaces/initialization.py +78 -0
- donna/{world → workspaces}/markdown.py +21 -26
- donna/{world → workspaces}/sources/base.py +2 -2
- donna/{world → workspaces}/sources/markdown.py +7 -6
- donna/{world → workspaces}/templates.py +4 -4
- donna/{world → workspaces}/tmp.py +19 -1
- donna/{world → workspaces}/worlds/base.py +5 -2
- donna/{world → workspaces}/worlds/filesystem.py +23 -9
- donna/{world → workspaces}/worlds/python.py +12 -9
- {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/METADATA +4 -1
- donna-0.2.1.dist-info/RECORD +92 -0
- {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/WHEEL +1 -1
- donna/artifacts/work/do_it.md +0 -142
- donna/artifacts/work/do_it_fast.md +0 -98
- donna/artifacts/work/planning.md +0 -245
- donna/cli/commands/projects.py +0 -49
- donna/world/artifacts.py +0 -122
- donna/world/initialization.py +0 -42
- donna/world/worlds/__init__.py +0 -0
- donna-0.2.0.dist-info/RECORD +0 -85
- /donna/{artifacts/work → workspaces}/__init__.py +0 -0
- /donna/{world → workspaces}/sources/__init__.py +0 -0
- /donna/{world → workspaces/worlds}/__init__.py +0 -0
- {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/entry_points.txt +0 -0
- {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, ClassVar
|
|
2
2
|
|
|
3
|
+
import pydantic
|
|
4
|
+
|
|
3
5
|
from donna.machine.artifacts import ArtifactSectionConfig
|
|
4
6
|
from donna.machine.primitives import Primitive
|
|
5
|
-
from donna.
|
|
7
|
+
from donna.workspaces.sources.markdown import MarkdownSectionMixin
|
|
6
8
|
|
|
7
9
|
if TYPE_CHECKING:
|
|
8
10
|
pass
|
|
@@ -16,5 +18,14 @@ class Text(MarkdownSectionMixin, Primitive):
|
|
|
16
18
|
config_class: ClassVar[type[TextConfig]] = TextConfig
|
|
17
19
|
|
|
18
20
|
|
|
21
|
+
class SpecificationConfig(ArtifactSectionConfig):
|
|
22
|
+
@pydantic.field_validator("tags", mode="after")
|
|
23
|
+
@classmethod
|
|
24
|
+
def ensure_specification_tag(cls, value: list[str]) -> list[str]:
|
|
25
|
+
if "specification" in value:
|
|
26
|
+
return value
|
|
27
|
+
return [*value, "specification"]
|
|
28
|
+
|
|
29
|
+
|
|
19
30
|
class Specification(MarkdownSectionMixin, Primitive):
|
|
20
|
-
|
|
31
|
+
config_class: ClassVar[type[SpecificationConfig]] = SpecificationConfig
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, ClassVar, Iterable, cast
|
|
2
2
|
|
|
3
|
+
import pydantic
|
|
4
|
+
|
|
3
5
|
from donna.core import errors as core_errors
|
|
4
6
|
from donna.core.errors import ErrorsList
|
|
5
7
|
from donna.core.result import Err, Ok, Result, unwrap_to_error
|
|
@@ -8,8 +10,8 @@ from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionCo
|
|
|
8
10
|
from donna.machine.errors import ArtifactValidationError
|
|
9
11
|
from donna.machine.operations import FsmMode, OperationMeta
|
|
10
12
|
from donna.machine.primitives import Primitive
|
|
11
|
-
from donna.
|
|
12
|
-
from donna.
|
|
13
|
+
from donna.workspaces import markdown
|
|
14
|
+
from donna.workspaces.sources.markdown import MarkdownSectionMixin
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
17
|
from donna.machine.changes import Change
|
|
@@ -97,6 +99,13 @@ def find_workflow_sections(start_operation_id: ArtifactSectionId, artifact: Arti
|
|
|
97
99
|
class WorkflowConfig(ArtifactSectionConfig):
|
|
98
100
|
start_operation_id: ArtifactSectionId
|
|
99
101
|
|
|
102
|
+
@pydantic.field_validator("tags", mode="after")
|
|
103
|
+
@classmethod
|
|
104
|
+
def ensure_workflow_tag(cls, value: list[str]) -> list[str]:
|
|
105
|
+
if "workflow" in value:
|
|
106
|
+
return value
|
|
107
|
+
return [*value, "workflow"]
|
|
108
|
+
|
|
100
109
|
|
|
101
110
|
class WorkflowMeta(ArtifactSectionMeta):
|
|
102
111
|
start_operation_id: ArtifactSectionId
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from jinja2.runtime import Context
|
|
4
|
+
|
|
5
|
+
from donna.core import errors as core_errors
|
|
6
|
+
from donna.core.errors import ErrorsList
|
|
7
|
+
from donna.core.result import Err, Ok, Result
|
|
8
|
+
from donna.domain.ids import FullArtifactIdPattern
|
|
9
|
+
from donna.machine.templates import Directive, PreparedDirectiveResult
|
|
10
|
+
from donna.protocol.modes import mode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnvironmentError(core_errors.EnvironmentError):
|
|
14
|
+
cell_kind: str = "directive_error"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ListInvalidArguments(EnvironmentError):
|
|
18
|
+
code: str = "donna.directives.list.invalid_arguments"
|
|
19
|
+
message: str = (
|
|
20
|
+
"List directive requires exactly one positional argument: artifact_id_pattern (got {error.provided_count})."
|
|
21
|
+
)
|
|
22
|
+
ways_to_fix: list[str] = ["Provide exactly one argument: artifact_id_pattern."]
|
|
23
|
+
provided_count: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ListInvalidKeyword(EnvironmentError):
|
|
27
|
+
code: str = "donna.directives.list.invalid_keyword"
|
|
28
|
+
message: str = "List directive accepts only the `tags` keyword argument (got {error.keyword})."
|
|
29
|
+
ways_to_fix: list[str] = ["Remove unsupported keyword arguments."]
|
|
30
|
+
keyword: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ListInvalidTags(EnvironmentError):
|
|
34
|
+
code: str = "donna.directives.list.invalid_tags"
|
|
35
|
+
message: str = "List directive `tags` must be a list of strings."
|
|
36
|
+
ways_to_fix: list[str] = ["Provide tags as a list of strings, e.g. tags=['tag1', 'tag2']."]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class List(Directive):
|
|
40
|
+
def _prepare_arguments( # noqa: CCR001
|
|
41
|
+
self,
|
|
42
|
+
context: Context,
|
|
43
|
+
*argv: Any,
|
|
44
|
+
**kwargs: Any,
|
|
45
|
+
) -> PreparedDirectiveResult:
|
|
46
|
+
if argv is None or len(argv) != 1:
|
|
47
|
+
return Err([ListInvalidArguments(provided_count=0 if argv is None else len(argv))])
|
|
48
|
+
|
|
49
|
+
for keyword in kwargs:
|
|
50
|
+
if keyword != "tags":
|
|
51
|
+
return Err([ListInvalidKeyword(keyword=keyword)])
|
|
52
|
+
|
|
53
|
+
artifact_pattern_result = FullArtifactIdPattern.parse(str(argv[0]))
|
|
54
|
+
errors = artifact_pattern_result.err()
|
|
55
|
+
if errors is not None:
|
|
56
|
+
return Err(errors)
|
|
57
|
+
|
|
58
|
+
artifact_pattern = artifact_pattern_result.ok()
|
|
59
|
+
assert artifact_pattern is not None
|
|
60
|
+
|
|
61
|
+
tags = kwargs.get("tags")
|
|
62
|
+
if tags is None:
|
|
63
|
+
tags_list: list[str] = []
|
|
64
|
+
elif isinstance(tags, (list, tuple, set)):
|
|
65
|
+
tags_list = [str(tag) for tag in tags]
|
|
66
|
+
else:
|
|
67
|
+
return Err([ListInvalidTags()])
|
|
68
|
+
|
|
69
|
+
return Ok((artifact_pattern, tags_list))
|
|
70
|
+
|
|
71
|
+
def render_view(
|
|
72
|
+
self, context: Context, artifact_pattern: FullArtifactIdPattern, tags: list[str]
|
|
73
|
+
) -> Result[Any, ErrorsList]:
|
|
74
|
+
protocol = mode().value
|
|
75
|
+
tags_args = " ".join(f"--tag '{tag}'" for tag in tags)
|
|
76
|
+
tag_suffix = f" {tags_args}" if tags_args else ""
|
|
77
|
+
return Ok(f"{artifact_pattern} (donna -p {protocol} artifacts list '{artifact_pattern}'{tag_suffix})")
|
|
78
|
+
|
|
79
|
+
def render_analyze(
|
|
80
|
+
self, context: Context, artifact_pattern: FullArtifactIdPattern, tags: list[str]
|
|
81
|
+
) -> Result[Any, ErrorsList]:
|
|
82
|
+
if not tags:
|
|
83
|
+
return Ok(f"$$donna {self.analyze_id} {artifact_pattern} donna$$")
|
|
84
|
+
|
|
85
|
+
tags_marker = ",".join(tags)
|
|
86
|
+
return Ok(f"$$donna {self.analyze_id} {artifact_pattern} tags={tags_marker} donna$$")
|
|
@@ -5,7 +5,7 @@ from jinja2.runtime import Context
|
|
|
5
5
|
from donna.core import errors as core_errors
|
|
6
6
|
from donna.core.errors import ErrorsList
|
|
7
7
|
from donna.core.result import Err, Ok, Result
|
|
8
|
-
from donna.domain.ids import
|
|
8
|
+
from donna.domain.ids import FullArtifactIdPattern
|
|
9
9
|
from donna.machine.templates import Directive, PreparedDirectiveResult
|
|
10
10
|
from donna.protocol.modes import mode
|
|
11
11
|
|
|
@@ -16,30 +16,71 @@ class EnvironmentError(core_errors.EnvironmentError):
|
|
|
16
16
|
|
|
17
17
|
class ViewInvalidArguments(EnvironmentError):
|
|
18
18
|
code: str = "donna.directives.view.invalid_arguments"
|
|
19
|
-
message: str =
|
|
20
|
-
|
|
19
|
+
message: str = (
|
|
20
|
+
"View directive requires exactly one positional argument: artifact_id_pattern (got {error.provided_count})."
|
|
21
|
+
)
|
|
22
|
+
ways_to_fix: list[str] = ["Provide exactly one argument: artifact_id_pattern."]
|
|
21
23
|
provided_count: int
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
class ViewInvalidKeyword(EnvironmentError):
|
|
27
|
+
code: str = "donna.directives.view.invalid_keyword"
|
|
28
|
+
message: str = "View directive accepts only the `tags` keyword argument (got {error.keyword})."
|
|
29
|
+
ways_to_fix: list[str] = ["Remove unsupported keyword arguments."]
|
|
30
|
+
keyword: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ViewInvalidTags(EnvironmentError):
|
|
34
|
+
code: str = "donna.directives.view.invalid_tags"
|
|
35
|
+
message: str = "View directive `tags` must be a list of strings."
|
|
36
|
+
ways_to_fix: list[str] = ["Provide tags as a list of strings, e.g. tags=['tag1', 'tag2']."]
|
|
37
|
+
|
|
38
|
+
|
|
24
39
|
class View(Directive):
|
|
25
|
-
def _prepare_arguments(
|
|
40
|
+
def _prepare_arguments( # noqa: CCR001
|
|
26
41
|
self,
|
|
27
42
|
context: Context,
|
|
28
43
|
*argv: Any,
|
|
44
|
+
**kwargs: Any,
|
|
29
45
|
) -> PreparedDirectiveResult:
|
|
30
46
|
if argv is None or len(argv) != 1:
|
|
31
47
|
return Err([ViewInvalidArguments(provided_count=0 if argv is None else len(argv))])
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
for keyword in kwargs:
|
|
50
|
+
if keyword != "tags":
|
|
51
|
+
return Err([ViewInvalidKeyword(keyword=keyword)])
|
|
52
|
+
|
|
53
|
+
artifact_pattern_result = FullArtifactIdPattern.parse(str(argv[0]))
|
|
54
|
+
errors = artifact_pattern_result.err()
|
|
35
55
|
if errors is not None:
|
|
36
56
|
return Err(errors)
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
assert
|
|
58
|
+
artifact_pattern = artifact_pattern_result.ok()
|
|
59
|
+
assert artifact_pattern is not None
|
|
60
|
+
|
|
61
|
+
tags = kwargs.get("tags")
|
|
62
|
+
if tags is None:
|
|
63
|
+
tags_list: list[str] = []
|
|
64
|
+
elif isinstance(tags, (list, tuple, set)):
|
|
65
|
+
tags_list = [str(tag) for tag in tags]
|
|
66
|
+
else:
|
|
67
|
+
return Err([ViewInvalidTags()])
|
|
40
68
|
|
|
41
|
-
return Ok((
|
|
69
|
+
return Ok((artifact_pattern, tags_list))
|
|
42
70
|
|
|
43
|
-
def render_view(
|
|
71
|
+
def render_view(
|
|
72
|
+
self, context: Context, artifact_pattern: FullArtifactIdPattern, tags: list[str]
|
|
73
|
+
) -> Result[Any, ErrorsList]:
|
|
44
74
|
protocol = mode().value
|
|
45
|
-
|
|
75
|
+
tags_args = " ".join(f"--tag '{tag}'" for tag in tags)
|
|
76
|
+
tag_suffix = f" {tags_args}" if tags_args else ""
|
|
77
|
+
return Ok(f"{artifact_pattern} (donna -p {protocol} artifacts view '{artifact_pattern}'{tag_suffix})")
|
|
78
|
+
|
|
79
|
+
def render_analyze(
|
|
80
|
+
self, context: Context, artifact_pattern: FullArtifactIdPattern, tags: list[str]
|
|
81
|
+
) -> Result[Any, ErrorsList]:
|
|
82
|
+
if not tags:
|
|
83
|
+
return Ok(f"$$donna {self.analyze_id} {artifact_pattern} donna$$")
|
|
84
|
+
|
|
85
|
+
tags_marker = ",".join(tags)
|
|
86
|
+
return Ok(f"$$donna {self.analyze_id} {artifact_pattern} tags={tags_marker} donna$$")
|
|
@@ -5,8 +5,10 @@ from donna.core.result import Ok, Result
|
|
|
5
5
|
from donna.domain.ids import FullArtifactId
|
|
6
6
|
from donna.machine.artifacts import ArtifactSection, ArtifactSectionConfig, ArtifactSectionMeta
|
|
7
7
|
from donna.machine.operations import FsmMode, OperationConfig, OperationKind, OperationMeta
|
|
8
|
-
from donna.
|
|
9
|
-
from donna.
|
|
8
|
+
from donna.protocol.cells import Cell
|
|
9
|
+
from donna.protocol.utils import instant_output
|
|
10
|
+
from donna.workspaces import markdown
|
|
11
|
+
from donna.workspaces.sources.markdown import MarkdownSectionMixin
|
|
10
12
|
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
14
|
from donna.machine.changes import Change
|
|
@@ -21,6 +23,15 @@ class FinishWorkflow(MarkdownSectionMixin, OperationKind):
|
|
|
21
23
|
def execute_section(self, task: "Task", unit: "WorkUnit", operation: ArtifactSection) -> Iterator["Change"]:
|
|
22
24
|
from donna.machine.changes import ChangeFinishTask
|
|
23
25
|
|
|
26
|
+
output_text = operation.description
|
|
27
|
+
|
|
28
|
+
output_cell = Cell.build_markdown(
|
|
29
|
+
kind="operation_output",
|
|
30
|
+
content=output_text,
|
|
31
|
+
operation_id=str(unit.operation_id),
|
|
32
|
+
)
|
|
33
|
+
instant_output([output_cell])
|
|
34
|
+
|
|
24
35
|
yield ChangeFinishTask(task_id=task.id)
|
|
25
36
|
|
|
26
37
|
config_class: ClassVar[type[FinishWorkflowConfig]] = FinishWorkflowConfig
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, ClassVar, Iterator, cast
|
|
2
|
+
|
|
3
|
+
from donna.core.errors import ErrorsList
|
|
4
|
+
from donna.core.result import Err, Ok, Result
|
|
5
|
+
from donna.domain.ids import ArtifactSectionId, FullArtifactId
|
|
6
|
+
from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionConfig, ArtifactSectionMeta
|
|
7
|
+
from donna.machine.errors import ArtifactValidationError
|
|
8
|
+
from donna.machine.operations import OperationConfig, OperationKind, OperationMeta
|
|
9
|
+
from donna.protocol.cells import Cell
|
|
10
|
+
from donna.protocol.utils import instant_output
|
|
11
|
+
from donna.workspaces import markdown
|
|
12
|
+
from donna.workspaces.sources.markdown import MarkdownSectionMixin
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from donna.machine.changes import Change
|
|
16
|
+
from donna.machine.tasks import Task, WorkUnit
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OutputMissingNextOperation(ArtifactValidationError):
|
|
20
|
+
code: str = "donna.workflows.output_missing_next_operation"
|
|
21
|
+
message: str = "Output operation `{error.section_id}` must define `next_operation`."
|
|
22
|
+
ways_to_fix: list[str] = [
|
|
23
|
+
'Add `next_operation = "<next_operation>"` to the operation config block.',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OutputConfig(OperationConfig):
|
|
28
|
+
next_operation: ArtifactSectionId | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OutputMeta(OperationMeta):
|
|
32
|
+
next_operation: ArtifactSectionId | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Output(MarkdownSectionMixin, OperationKind):
|
|
36
|
+
config_class: ClassVar[type[OutputConfig]] = OutputConfig
|
|
37
|
+
|
|
38
|
+
def markdown_construct_meta(
|
|
39
|
+
self,
|
|
40
|
+
artifact_id: "FullArtifactId",
|
|
41
|
+
source: markdown.SectionSource,
|
|
42
|
+
section_config: ArtifactSectionConfig,
|
|
43
|
+
description: str,
|
|
44
|
+
primary: bool = False,
|
|
45
|
+
) -> Result[ArtifactSectionMeta, ErrorsList]:
|
|
46
|
+
output_config = cast(OutputConfig, section_config)
|
|
47
|
+
|
|
48
|
+
allowed_transitions: set[ArtifactSectionId] = set()
|
|
49
|
+
if output_config.next_operation is not None:
|
|
50
|
+
allowed_transitions.add(output_config.next_operation)
|
|
51
|
+
|
|
52
|
+
return Ok(
|
|
53
|
+
OutputMeta(
|
|
54
|
+
fsm_mode=output_config.fsm_mode,
|
|
55
|
+
allowed_transtions=allowed_transitions,
|
|
56
|
+
next_operation=output_config.next_operation,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def execute_section(self, task: "Task", unit: "WorkUnit", operation: ArtifactSection) -> Iterator["Change"]:
|
|
61
|
+
from donna.machine.changes import ChangeAddWorkUnit
|
|
62
|
+
|
|
63
|
+
meta = cast(OutputMeta, operation.meta)
|
|
64
|
+
|
|
65
|
+
output_text = operation.description
|
|
66
|
+
|
|
67
|
+
output_cell = Cell.build_markdown(
|
|
68
|
+
kind="operation_output",
|
|
69
|
+
content=output_text,
|
|
70
|
+
operation_id=str(unit.operation_id),
|
|
71
|
+
)
|
|
72
|
+
instant_output([output_cell])
|
|
73
|
+
|
|
74
|
+
next_operation = meta.next_operation
|
|
75
|
+
assert next_operation is not None
|
|
76
|
+
full_operation_id = unit.operation_id.full_artifact_id.to_full_local(next_operation)
|
|
77
|
+
|
|
78
|
+
yield ChangeAddWorkUnit(task_id=task.id, operation_id=full_operation_id)
|
|
79
|
+
|
|
80
|
+
def validate_section(self, artifact: Artifact, section_id: ArtifactSectionId) -> Result[None, ErrorsList]:
|
|
81
|
+
section = artifact.get_section(section_id).unwrap()
|
|
82
|
+
meta = cast(OutputMeta, section.meta)
|
|
83
|
+
|
|
84
|
+
if meta.next_operation is None:
|
|
85
|
+
return Err([OutputMissingNextOperation(artifact_id=artifact.id, section_id=section_id)])
|
|
86
|
+
|
|
87
|
+
return Ok(None)
|
|
@@ -10,8 +10,8 @@ from donna.domain.ids import ArtifactSectionId, FullArtifactId
|
|
|
10
10
|
from donna.machine.action_requests import ActionRequest
|
|
11
11
|
from donna.machine.artifacts import ArtifactSection, ArtifactSectionConfig, ArtifactSectionMeta
|
|
12
12
|
from donna.machine.operations import FsmMode, OperationConfig, OperationKind, OperationMeta
|
|
13
|
-
from donna.
|
|
14
|
-
from donna.
|
|
13
|
+
from donna.workspaces import markdown
|
|
14
|
+
from donna.workspaces.sources.markdown import MarkdownSectionMixin
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from donna.machine.changes import Change
|
|
@@ -74,13 +74,7 @@ class RequestAction(MarkdownSectionMixin, OperationKind):
|
|
|
74
74
|
def execute_section(self, task: "Task", unit: "WorkUnit", operation: ArtifactSection) -> Iterator["Change"]:
|
|
75
75
|
from donna.machine.changes import ChangeAddActionRequest
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
"scheme": operation,
|
|
79
|
-
"task": task,
|
|
80
|
-
"work_unit": unit,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
request_text = operation.description.format(**context)
|
|
77
|
+
request_text = operation.description
|
|
84
78
|
|
|
85
79
|
full_operation_id = unit.operation_id
|
|
86
80
|
|
|
@@ -12,8 +12,8 @@ from donna.domain.ids import ArtifactSectionId, FullArtifactId
|
|
|
12
12
|
from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionConfig, ArtifactSectionMeta
|
|
13
13
|
from donna.machine.errors import ArtifactValidationError
|
|
14
14
|
from donna.machine.operations import OperationConfig, OperationKind, OperationMeta
|
|
15
|
-
from donna.
|
|
16
|
-
from donna.
|
|
15
|
+
from donna.workspaces import markdown
|
|
16
|
+
from donna.workspaces.sources.markdown import MarkdownSectionMixin
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from donna.machine.changes import Change
|
donna/protocol/utils.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from donna.protocol.cells import Cell
|
|
4
|
+
from donna.protocol.modes import get_cell_formatter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def instant_output(cells: list[Cell]) -> None:
|
|
8
|
+
if not cells:
|
|
9
|
+
return
|
|
10
|
+
|
|
11
|
+
formatter = get_cell_formatter()
|
|
12
|
+
|
|
13
|
+
formatted_cells: list[bytes] = []
|
|
14
|
+
for cell in cells:
|
|
15
|
+
# TODO: we should refactor that hardcoded check somehow
|
|
16
|
+
if cell.kind == "donna_log":
|
|
17
|
+
formatted_cells.append(formatter.format_log(cell, single_mode=True))
|
|
18
|
+
else:
|
|
19
|
+
formatted_cells.append(formatter.format_cell(cell, single_mode=False))
|
|
20
|
+
|
|
21
|
+
sys.stdout.buffer.write(b"\n\n".join(formatted_cells) + b"\n\n")
|
|
22
|
+
sys.stdout.buffer.flush()
|
|
@@ -0,0 +1,238 @@
|
|
|
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.workspaces import errors
|
|
10
|
+
from donna.workspaces.config import config
|
|
11
|
+
from donna.workspaces.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.WorkspaceError):
|
|
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.workspaces.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 ArtifactRemoveError(errors.WorkspaceError):
|
|
36
|
+
cell_kind: str = "artifact_remove_error"
|
|
37
|
+
artifact_id: FullArtifactId
|
|
38
|
+
|
|
39
|
+
def content_intro(self) -> str:
|
|
40
|
+
return f"Error removing artifact '{self.artifact_id}'"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CanNotRemoveReadonlyWorld(ArtifactRemoveError):
|
|
44
|
+
code: str = "donna.workspaces.cannot_remove_readonly_world"
|
|
45
|
+
message: str = "Cannot remove artifact from the read-only world `{error.world_id}`"
|
|
46
|
+
world_id: WorldId
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class InputPathHasNoExtension(ArtifactUpdateError):
|
|
50
|
+
code: str = "donna.workspaces.input_path_has_no_extension"
|
|
51
|
+
message: str = "Input path has no extension to determine artifact source type"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NoSourceForArtifactExtension(ArtifactUpdateError):
|
|
55
|
+
code: str = "donna.workspaces.no_source_for_artifact_extension"
|
|
56
|
+
message: str = "No source found for artifact extension of input path"
|
|
57
|
+
extension: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ArtifactCopyError(errors.WorkspaceError):
|
|
61
|
+
cell_kind: str = "artifact_copy_error"
|
|
62
|
+
source_id: FullArtifactId
|
|
63
|
+
target_id: FullArtifactId
|
|
64
|
+
|
|
65
|
+
def content_intro(self) -> str:
|
|
66
|
+
return f"Error copying artifact '{self.source_id}' to '{self.target_id}'"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CanNotCopyToReadonlyWorld(ArtifactCopyError):
|
|
70
|
+
code: str = "donna.workspaces.cannot_copy_to_readonly_world"
|
|
71
|
+
message: str = "Cannot copy artifact to the read-only world `{error.world_id}`"
|
|
72
|
+
world_id: WorldId
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SourceArtifactHasNoExtension(ArtifactCopyError):
|
|
76
|
+
code: str = "donna.workspaces.source_artifact_has_no_extension"
|
|
77
|
+
message: str = "Source artifact has no extension to determine source type"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@unwrap_to_error
|
|
81
|
+
def artifact_file_extension(full_id: FullArtifactId) -> Result[str, ErrorsList]:
|
|
82
|
+
world = config().get_world(full_id.world_id).unwrap()
|
|
83
|
+
return Ok(world.file_extension_for(full_id.artifact_id).unwrap().lstrip("."))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@unwrap_to_error
|
|
87
|
+
def fetch_artifact(full_id: FullArtifactId, output: pathlib.Path) -> Result[None, ErrorsList]:
|
|
88
|
+
world = config().get_world(full_id.world_id).unwrap()
|
|
89
|
+
content = world.fetch_source(full_id.artifact_id).unwrap()
|
|
90
|
+
|
|
91
|
+
with output.open("wb") as f:
|
|
92
|
+
f.write(content)
|
|
93
|
+
|
|
94
|
+
return Ok(None)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@unwrap_to_error
|
|
98
|
+
def update_artifact(full_id: FullArtifactId, input: pathlib.Path) -> Result[None, ErrorsList]:
|
|
99
|
+
world = config().get_world(full_id.world_id).unwrap()
|
|
100
|
+
|
|
101
|
+
if world.readonly:
|
|
102
|
+
return Err([CanNotUpdateReadonlyWorld(artifact_id=full_id, path=input, world_id=world.id)])
|
|
103
|
+
|
|
104
|
+
source_suffix = input.suffix.lower()
|
|
105
|
+
content_bytes = input.read_bytes()
|
|
106
|
+
|
|
107
|
+
if not source_suffix:
|
|
108
|
+
return Err([InputPathHasNoExtension(artifact_id=full_id, path=input)])
|
|
109
|
+
|
|
110
|
+
source_config = config().find_source_for_extension(source_suffix)
|
|
111
|
+
if source_config is None:
|
|
112
|
+
return Err([NoSourceForArtifactExtension(artifact_id=full_id, path=input, extension=source_suffix)])
|
|
113
|
+
|
|
114
|
+
render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
|
|
115
|
+
test_artifact = source_config.construct_artifact_from_bytes(full_id, content_bytes, render_context).unwrap()
|
|
116
|
+
validation_result = test_artifact.validate_artifact()
|
|
117
|
+
|
|
118
|
+
validation_result.unwrap()
|
|
119
|
+
world.update(full_id.artifact_id, content_bytes, source_suffix).unwrap()
|
|
120
|
+
|
|
121
|
+
return Ok(None)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@unwrap_to_error
|
|
125
|
+
def copy_artifact(source_id: FullArtifactId, target_id: FullArtifactId) -> Result[None, ErrorsList]:
|
|
126
|
+
source_world = config().get_world(source_id.world_id).unwrap()
|
|
127
|
+
target_world = config().get_world(target_id.world_id).unwrap()
|
|
128
|
+
|
|
129
|
+
if target_world.readonly:
|
|
130
|
+
return Err(
|
|
131
|
+
[
|
|
132
|
+
CanNotCopyToReadonlyWorld(
|
|
133
|
+
source_id=source_id,
|
|
134
|
+
target_id=target_id,
|
|
135
|
+
world_id=target_world.id,
|
|
136
|
+
)
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
content_bytes = source_world.fetch_source(source_id.artifact_id).unwrap()
|
|
141
|
+
source_extension = source_world.file_extension_for(source_id.artifact_id).unwrap()
|
|
142
|
+
|
|
143
|
+
if not source_extension:
|
|
144
|
+
return Err([SourceArtifactHasNoExtension(source_id=source_id, target_id=target_id)])
|
|
145
|
+
|
|
146
|
+
source_extension = source_extension.lower()
|
|
147
|
+
source_config = config().find_source_for_extension(source_extension)
|
|
148
|
+
if source_config is None:
|
|
149
|
+
return Err(
|
|
150
|
+
[
|
|
151
|
+
NoSourceForArtifactExtension(
|
|
152
|
+
artifact_id=source_id,
|
|
153
|
+
path=pathlib.Path(str(source_id)),
|
|
154
|
+
extension=source_extension,
|
|
155
|
+
)
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
|
|
160
|
+
test_artifact = source_config.construct_artifact_from_bytes(target_id, content_bytes, render_context).unwrap()
|
|
161
|
+
test_artifact.validate_artifact().unwrap()
|
|
162
|
+
|
|
163
|
+
target_world.update(target_id.artifact_id, content_bytes, source_extension).unwrap()
|
|
164
|
+
return Ok(None)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@unwrap_to_error
|
|
168
|
+
def move_artifact(source_id: FullArtifactId, target_id: FullArtifactId) -> Result[None, ErrorsList]:
|
|
169
|
+
copy_result = copy_artifact(source_id, target_id)
|
|
170
|
+
if copy_result.is_err():
|
|
171
|
+
return copy_result
|
|
172
|
+
|
|
173
|
+
return remove_artifact(source_id)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@unwrap_to_error
|
|
177
|
+
def remove_artifact(full_id: FullArtifactId) -> Result[None, ErrorsList]:
|
|
178
|
+
world = config().get_world(full_id.world_id).unwrap()
|
|
179
|
+
|
|
180
|
+
if world.readonly:
|
|
181
|
+
return Err([CanNotRemoveReadonlyWorld(artifact_id=full_id, world_id=world.id)])
|
|
182
|
+
|
|
183
|
+
world.remove(full_id.artifact_id).unwrap()
|
|
184
|
+
return Ok(None)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@unwrap_to_error
|
|
188
|
+
def load_artifact(
|
|
189
|
+
full_id: FullArtifactId, render_context: ArtifactRenderContext | None = None
|
|
190
|
+
) -> Result[Artifact, ErrorsList]:
|
|
191
|
+
if render_context is None:
|
|
192
|
+
render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
|
|
193
|
+
|
|
194
|
+
world = config().get_world(full_id.world_id).unwrap()
|
|
195
|
+
return Ok(world.fetch(full_id.artifact_id, render_context).unwrap())
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def list_artifacts( # noqa: CCR001
|
|
199
|
+
pattern: FullArtifactIdPattern,
|
|
200
|
+
render_context: ArtifactRenderContext | None = None,
|
|
201
|
+
tags: list[str] | None = None,
|
|
202
|
+
) -> Result[list[Artifact], ErrorsList]:
|
|
203
|
+
if render_context is None:
|
|
204
|
+
render_context = ArtifactRenderContext(primary_mode=RenderMode.view)
|
|
205
|
+
|
|
206
|
+
tag_filters = tags or []
|
|
207
|
+
|
|
208
|
+
artifacts: list[Artifact] = []
|
|
209
|
+
errors: ErrorsList = []
|
|
210
|
+
|
|
211
|
+
for world in reversed(config().worlds_instances):
|
|
212
|
+
for artifact_id in world.list_artifacts(pattern):
|
|
213
|
+
full_id = FullArtifactId((world.id, artifact_id))
|
|
214
|
+
artifact_result = load_artifact(full_id, render_context)
|
|
215
|
+
if artifact_result.is_err():
|
|
216
|
+
errors.extend(artifact_result.unwrap_err())
|
|
217
|
+
continue
|
|
218
|
+
artifact = artifact_result.unwrap()
|
|
219
|
+
if tag_filters and not _artifact_matches_tags(artifact, tag_filters):
|
|
220
|
+
continue
|
|
221
|
+
artifacts.append(artifact)
|
|
222
|
+
|
|
223
|
+
if errors:
|
|
224
|
+
return Err(errors)
|
|
225
|
+
|
|
226
|
+
return Ok(artifacts)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _artifact_matches_tags(artifact: Artifact, tags: list[str]) -> bool:
|
|
230
|
+
if not tags:
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
primary_result = artifact.primary_section()
|
|
234
|
+
if primary_result.is_err():
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
primary = primary_result.unwrap()
|
|
238
|
+
return all(tag in primary.tags for tag in tags)
|
|
@@ -3,7 +3,7 @@ from functools import lru_cache
|
|
|
3
3
|
from typing import Iterable, Protocol
|
|
4
4
|
|
|
5
5
|
from donna.domain.ids import ArtifactId, FullArtifactId, FullArtifactIdPattern, WorldId
|
|
6
|
-
from donna.
|
|
6
|
+
from donna.workspaces.config import config
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class ArtifactListingNode(Protocol):
|