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.
Files changed (68) hide show
  1. donna/artifacts/intro.md +39 -0
  2. donna/artifacts/research/specs/report.md +163 -0
  3. donna/artifacts/research/work/research.md +198 -0
  4. donna/artifacts/rfc/specs/request_for_change.md +271 -0
  5. donna/artifacts/rfc/work/create.md +120 -0
  6. donna/artifacts/rfc/work/do.md +109 -0
  7. donna/artifacts/rfc/work/plan.md +68 -0
  8. donna/artifacts/usage/artifacts.md +41 -6
  9. donna/artifacts/usage/cli.md +106 -37
  10. donna/artifacts/usage/worlds.md +8 -2
  11. donna/cli/__main__.py +1 -1
  12. donna/cli/commands/artifacts.py +104 -17
  13. donna/cli/commands/sessions.py +7 -7
  14. donna/cli/commands/workspaces.py +42 -0
  15. donna/cli/errors.py +18 -0
  16. donna/cli/types.py +16 -9
  17. donna/cli/utils.py +2 -2
  18. donna/core/errors.py +1 -11
  19. donna/core/result.py +5 -8
  20. donna/core/utils.py +0 -3
  21. donna/lib/__init__.py +4 -0
  22. donna/lib/sources.py +1 -1
  23. donna/lib/worlds.py +2 -2
  24. donna/machine/action_requests.py +0 -5
  25. donna/machine/artifacts.py +8 -6
  26. donna/machine/primitives.py +4 -4
  27. donna/machine/sessions.py +12 -4
  28. donna/machine/state.py +2 -2
  29. donna/machine/tasks.py +4 -18
  30. donna/machine/templates.py +4 -2
  31. donna/primitives/artifacts/specification.py +13 -2
  32. donna/primitives/artifacts/workflow.py +11 -2
  33. donna/primitives/directives/list.py +86 -0
  34. donna/primitives/directives/view.py +52 -11
  35. donna/primitives/operations/finish_workflow.py +13 -2
  36. donna/primitives/operations/output.py +87 -0
  37. donna/primitives/operations/request_action.py +3 -9
  38. donna/primitives/operations/run_script.py +2 -2
  39. donna/protocol/utils.py +22 -0
  40. donna/workspaces/artifacts.py +238 -0
  41. donna/{world → workspaces}/artifacts_discovery.py +1 -1
  42. donna/{world → workspaces}/config.py +13 -6
  43. donna/{world → workspaces}/errors.py +55 -45
  44. donna/workspaces/initialization.py +78 -0
  45. donna/{world → workspaces}/markdown.py +21 -26
  46. donna/{world → workspaces}/sources/base.py +2 -2
  47. donna/{world → workspaces}/sources/markdown.py +7 -6
  48. donna/{world → workspaces}/templates.py +4 -4
  49. donna/{world → workspaces}/tmp.py +19 -1
  50. donna/{world → workspaces}/worlds/base.py +5 -2
  51. donna/{world → workspaces}/worlds/filesystem.py +23 -9
  52. donna/{world → workspaces}/worlds/python.py +12 -9
  53. {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/METADATA +4 -1
  54. donna-0.2.1.dist-info/RECORD +92 -0
  55. {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/WHEEL +1 -1
  56. donna/artifacts/work/do_it.md +0 -142
  57. donna/artifacts/work/do_it_fast.md +0 -98
  58. donna/artifacts/work/planning.md +0 -245
  59. donna/cli/commands/projects.py +0 -49
  60. donna/world/artifacts.py +0 -122
  61. donna/world/initialization.py +0 -42
  62. donna/world/worlds/__init__.py +0 -0
  63. donna-0.2.0.dist-info/RECORD +0 -85
  64. /donna/{artifacts/work → workspaces}/__init__.py +0 -0
  65. /donna/{world → workspaces}/sources/__init__.py +0 -0
  66. /donna/{world → workspaces/worlds}/__init__.py +0 -0
  67. {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/entry_points.txt +0 -0
  68. {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.world.sources.markdown import MarkdownSectionMixin
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
- pass
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.world import markdown
12
- from donna.world.sources.markdown import MarkdownSectionMixin
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 FullArtifactId
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 = "View directive requires exactly one argument: specification_id (got {error.provided_count})."
20
- ways_to_fix: list[str] = ["Provide exactly one argument: specification_id."]
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
- artifact_id_result = FullArtifactId.parse(str(argv[0]))
34
- errors = artifact_id_result.err()
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
- artifact_id = artifact_id_result.ok()
39
- assert artifact_id is not None
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((artifact_id,))
69
+ return Ok((artifact_pattern, tags_list))
42
70
 
43
- def render_view(self, context: Context, specification_id: FullArtifactId) -> Result[Any, ErrorsList]:
71
+ def render_view(
72
+ self, context: Context, artifact_pattern: FullArtifactIdPattern, tags: list[str]
73
+ ) -> Result[Any, ErrorsList]:
44
74
  protocol = mode().value
45
- return Ok(f"donna -p {protocol} artifacts view '{specification_id}'")
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.world import markdown
9
- from donna.world.sources.markdown import MarkdownSectionMixin
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.world import markdown
14
- from donna.world.sources.markdown import MarkdownSectionMixin
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
- context: dict[str, object] = {
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.world import markdown
16
- from donna.world.sources.markdown import MarkdownSectionMixin
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
@@ -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.world.config import config
6
+ from donna.workspaces.config import config
7
7
 
8
8
 
9
9
  class ArtifactListingNode(Protocol):