donna 0.2.0__py3-none-any.whl → 0.2.2__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 +270 -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 +55 -12
- donna/artifacts/usage/cli.md +114 -39
- 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 +8 -8
- 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 +5 -5
- donna/machine/sessions.py +13 -5
- donna/machine/state.py +4 -4
- 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 +18 -11
- 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 +8 -7
- donna/{world → workspaces}/templates.py +4 -4
- donna/workspaces/tmp.py +51 -0
- donna/{world → workspaces}/worlds/base.py +6 -3
- donna/{world → workspaces}/worlds/filesystem.py +30 -10
- donna/{world → workspaces}/worlds/python.py +12 -9
- donna-0.2.2.dist-info/METADATA +463 -0
- donna-0.2.2.dist-info/RECORD +92 -0
- {donna-0.2.0.dist-info → donna-0.2.2.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/tmp.py +0 -33
- donna/world/worlds/__init__.py +0 -0
- donna-0.2.0.dist-info/METADATA +0 -44
- 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.2.dist-info}/entry_points.txt +0 -0
- {donna-0.2.0.dist-info → donna-0.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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):
|
|
@@ -8,11 +8,11 @@ from donna.core.errors import ErrorsList
|
|
|
8
8
|
from donna.core.result import Err, Ok, Result
|
|
9
9
|
from donna.domain.ids import PythonImportPath, WorldId
|
|
10
10
|
from donna.machine.primitives import resolve_primitive
|
|
11
|
-
from donna.
|
|
12
|
-
from donna.
|
|
13
|
-
from donna.
|
|
14
|
-
from donna.
|
|
15
|
-
from donna.
|
|
11
|
+
from donna.workspaces import errors as world_errors
|
|
12
|
+
from donna.workspaces.sources.base import SourceConfig as SourceConfigValue
|
|
13
|
+
from donna.workspaces.sources.base import SourceConstructor
|
|
14
|
+
from donna.workspaces.worlds.base import World as BaseWorld
|
|
15
|
+
from donna.workspaces.worlds.base import WorldConstructor
|
|
16
16
|
|
|
17
17
|
DONNA_DIR_NAME = ".donna"
|
|
18
18
|
DONNA_CONFIG_NAME = "config.toml"
|
|
@@ -46,7 +46,7 @@ def _default_sources() -> list[SourceConfig]:
|
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def _create_default_worlds(
|
|
49
|
+
def _create_default_worlds() -> list[WorldConfig]:
|
|
50
50
|
return [
|
|
51
51
|
WorldConfig.model_validate(
|
|
52
52
|
{
|
|
@@ -63,7 +63,7 @@ def _create_default_worlds(project_dir: pathlib.Path) -> list[WorldConfig]:
|
|
|
63
63
|
"kind": "donna.lib.worlds.filesystem",
|
|
64
64
|
"readonly": True,
|
|
65
65
|
"session": False,
|
|
66
|
-
"path":
|
|
66
|
+
"path": f"~/{DONNA_DIR_NAME}/{DONNA_WORLD_HOME_DIR_NAME}",
|
|
67
67
|
}
|
|
68
68
|
),
|
|
69
69
|
WorldConfig.model_validate(
|
|
@@ -72,7 +72,7 @@ def _create_default_worlds(project_dir: pathlib.Path) -> list[WorldConfig]:
|
|
|
72
72
|
"kind": "donna.lib.worlds.filesystem",
|
|
73
73
|
"readonly": False,
|
|
74
74
|
"session": False,
|
|
75
|
-
"path":
|
|
75
|
+
"path": pathlib.Path(DONNA_DIR_NAME) / DONNA_WORLD_PROJECT_DIR_NAME,
|
|
76
76
|
}
|
|
77
77
|
),
|
|
78
78
|
WorldConfig.model_validate(
|
|
@@ -81,14 +81,14 @@ def _create_default_worlds(project_dir: pathlib.Path) -> list[WorldConfig]:
|
|
|
81
81
|
"kind": "donna.lib.worlds.filesystem",
|
|
82
82
|
"readonly": False,
|
|
83
83
|
"session": True,
|
|
84
|
-
"path":
|
|
84
|
+
"path": pathlib.Path(DONNA_DIR_NAME) / DONNA_WORLD_SESSION_DIR_NAME,
|
|
85
85
|
}
|
|
86
86
|
),
|
|
87
87
|
]
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def _default_worlds() -> list[WorldConfig]:
|
|
91
|
-
return _create_default_worlds(
|
|
91
|
+
return _create_default_worlds()
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
class Config(BaseEntity):
|
|
@@ -152,7 +152,14 @@ class Config(BaseEntity):
|
|
|
152
152
|
if source.kind == kind:
|
|
153
153
|
return Ok(source)
|
|
154
154
|
|
|
155
|
-
return Err(
|
|
155
|
+
return Err(
|
|
156
|
+
[
|
|
157
|
+
world_errors.SourceConfigNotConfigured(
|
|
158
|
+
source_id=kind,
|
|
159
|
+
kind=kind,
|
|
160
|
+
)
|
|
161
|
+
]
|
|
162
|
+
)
|
|
156
163
|
|
|
157
164
|
def find_source_for_extension(self, extension: str) -> SourceConfigValue | None:
|
|
158
165
|
for source in self._sources_instances:
|
|
@@ -5,64 +5,82 @@ from donna.domain.ids import ArtifactId, FullArtifactId, WorldId
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class InternalError(core_errors.InternalError):
|
|
8
|
-
"""Base class for internal errors in donna.
|
|
8
|
+
"""Base class for internal errors in donna.workspaces."""
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class
|
|
12
|
-
cell_kind: str = "
|
|
11
|
+
class WorkspaceError(core_errors.EnvironmentError):
|
|
12
|
+
cell_kind: str = "workspace_error"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class
|
|
16
|
-
cell_kind: str = "
|
|
15
|
+
class WorkspaceConfigError(WorkspaceError):
|
|
16
|
+
cell_kind: str = "workspace_config_error"
|
|
17
17
|
config_path: pathlib.Path
|
|
18
18
|
|
|
19
19
|
def content_intro(self) -> str:
|
|
20
|
-
return f"Error in
|
|
20
|
+
return f"Error in workspace config file '{self.config_path}'"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class ConfigParseFailed(
|
|
24
|
-
code: str = "donna.
|
|
23
|
+
class ConfigParseFailed(WorkspaceConfigError):
|
|
24
|
+
code: str = "donna.workspaces.config_parse_failed"
|
|
25
25
|
message: str = "Failed to parse config file: {error.details}"
|
|
26
26
|
details: str
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
class ConfigValidationFailed(
|
|
30
|
-
code: str = "donna.
|
|
29
|
+
class ConfigValidationFailed(WorkspaceConfigError):
|
|
30
|
+
code: str = "donna.workspaces.config_validation_failed"
|
|
31
31
|
message: str = "Failed to validate config file: {error.details}"
|
|
32
32
|
details: str
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class WorkspaceAlreadyInitialized(WorkspaceError):
|
|
36
|
+
code: str = "donna.workspaces.workspace_already_initialized"
|
|
37
|
+
message: str = "Workspace already initialized at `{error.project_dir}`"
|
|
38
|
+
ways_to_fix: list[str] = [
|
|
39
|
+
"Continue using the existing workspace.",
|
|
40
|
+
"Remove the existing `.donna` directory if you want to reinitialize.",
|
|
41
|
+
"Choose a different project directory.",
|
|
42
|
+
]
|
|
43
|
+
project_dir: pathlib.Path
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WorldError(WorkspaceError):
|
|
47
|
+
cell_kind: str = "world_error"
|
|
48
|
+
world_id: WorldId
|
|
49
|
+
|
|
50
|
+
|
|
35
51
|
class WorldNotConfigured(WorldError):
|
|
36
|
-
code: str = "donna.
|
|
52
|
+
code: str = "donna.workspaces.world_not_configured"
|
|
37
53
|
message: str = "World with id `{error.world_id}` is not configured"
|
|
38
|
-
world_id: WorldId
|
|
39
54
|
|
|
40
55
|
|
|
41
|
-
class
|
|
42
|
-
|
|
56
|
+
class SourceError(WorkspaceError):
|
|
57
|
+
cell_kind: str = "source_error"
|
|
58
|
+
source_id: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SourceConfigNotConfigured(SourceError):
|
|
62
|
+
code: str = "donna.workspaces.source_config_not_configured"
|
|
43
63
|
message: str = "Source config `{error.kind}` is not configured"
|
|
44
64
|
kind: str
|
|
45
65
|
|
|
46
66
|
|
|
47
67
|
class WorldReadonly(WorldError):
|
|
48
|
-
code: str = "donna.
|
|
68
|
+
code: str = "donna.workspaces.world_readonly"
|
|
49
69
|
message: str = "World `{error.world_id}` is read-only"
|
|
50
70
|
ways_to_fix: list[str] = [
|
|
51
71
|
"Use a world configured with readonly = false. Most likely they are `project` and `session`.",
|
|
52
72
|
]
|
|
53
|
-
world_id: WorldId
|
|
54
73
|
|
|
55
74
|
|
|
56
75
|
class WorldStateStorageUnsupported(WorldError):
|
|
57
|
-
code: str = "donna.
|
|
76
|
+
code: str = "donna.workspaces.state_storage_unsupported"
|
|
58
77
|
message: str = "World `{error.world_id}` does not support state storage"
|
|
59
78
|
ways_to_fix: list[str] = [
|
|
60
79
|
"Use the session world.",
|
|
61
80
|
]
|
|
62
|
-
world_id: WorldId
|
|
63
81
|
|
|
64
82
|
|
|
65
|
-
class ArtifactError(
|
|
83
|
+
class ArtifactError(WorkspaceError):
|
|
66
84
|
cell_kind: str = "artifact_error"
|
|
67
85
|
artifact_id: ArtifactId
|
|
68
86
|
world_id: WorldId
|
|
@@ -72,7 +90,7 @@ class ArtifactError(WorldError):
|
|
|
72
90
|
|
|
73
91
|
|
|
74
92
|
class ArtifactNotFound(ArtifactError):
|
|
75
|
-
code: str = "donna.
|
|
93
|
+
code: str = "donna.workspaces.artifact_not_found"
|
|
76
94
|
message: str = "Artifact `{error.artifact_id}` does not exist in world `{error.world_id}`"
|
|
77
95
|
ways_to_fix: list[str] = [
|
|
78
96
|
"Check the artifact id for typos.",
|
|
@@ -81,7 +99,7 @@ class ArtifactNotFound(ArtifactError):
|
|
|
81
99
|
|
|
82
100
|
|
|
83
101
|
class ArtifactMultipleFiles(ArtifactError):
|
|
84
|
-
code: str = "donna.
|
|
102
|
+
code: str = "donna.workspaces.artifact_multiple_files"
|
|
85
103
|
message: str = "Artifact `{error.artifact_id}` has multiple files in world `{error.world_id}`"
|
|
86
104
|
ways_to_fix: list[str] = [
|
|
87
105
|
"Keep a single source file per artifact in the world.",
|
|
@@ -89,7 +107,7 @@ class ArtifactMultipleFiles(ArtifactError):
|
|
|
89
107
|
|
|
90
108
|
|
|
91
109
|
class UnsupportedArtifactSourceExtension(ArtifactError):
|
|
92
|
-
code: str = "donna.
|
|
110
|
+
code: str = "donna.workspaces.unsupported_artifact_source_extension"
|
|
93
111
|
message: str = "Unsupported artifact source extension `{error.extension}` in world `{error.world_id}`"
|
|
94
112
|
ways_to_fix: list[str] = [
|
|
95
113
|
"Use a supported extension for the configured sources.",
|
|
@@ -97,7 +115,7 @@ class UnsupportedArtifactSourceExtension(ArtifactError):
|
|
|
97
115
|
extension: str
|
|
98
116
|
|
|
99
117
|
|
|
100
|
-
class MarkdownError(
|
|
118
|
+
class MarkdownError(WorkspaceError):
|
|
101
119
|
cell_kind: str = "markdown_error"
|
|
102
120
|
artifact_id: FullArtifactId | None = None
|
|
103
121
|
|
|
@@ -108,7 +126,7 @@ class MarkdownError(WorldError):
|
|
|
108
126
|
return f"Error in markdown artifact '{self.artifact_id}'"
|
|
109
127
|
|
|
110
128
|
|
|
111
|
-
class TemplateDirectiveError(
|
|
129
|
+
class TemplateDirectiveError(WorkspaceError):
|
|
112
130
|
cell_kind: str = "template_directive_error"
|
|
113
131
|
artifact_id: FullArtifactId | None = None
|
|
114
132
|
|
|
@@ -120,14 +138,14 @@ class TemplateDirectiveError(WorldError):
|
|
|
120
138
|
|
|
121
139
|
|
|
122
140
|
class DirectivePathIncomplete(TemplateDirectiveError):
|
|
123
|
-
code: str = "donna.
|
|
141
|
+
code: str = "donna.workspaces.directive_path_incomplete"
|
|
124
142
|
message: str = "Directive path must include module and directive parts, got `{error.path}`."
|
|
125
143
|
ways_to_fix: list[str] = ["Use a directive path with both module and directive names."]
|
|
126
144
|
path: str
|
|
127
145
|
|
|
128
146
|
|
|
129
147
|
class DirectiveModuleNotImportable(TemplateDirectiveError):
|
|
130
|
-
code: str = "donna.
|
|
148
|
+
code: str = "donna.workspaces.directive_module_not_importable"
|
|
131
149
|
message: str = "Directive module `{error.module_path}` is not importable."
|
|
132
150
|
ways_to_fix: list[str] = [
|
|
133
151
|
"Check the module path for typos.",
|
|
@@ -137,7 +155,7 @@ class DirectiveModuleNotImportable(TemplateDirectiveError):
|
|
|
137
155
|
|
|
138
156
|
|
|
139
157
|
class DirectiveNotAvailable(TemplateDirectiveError):
|
|
140
|
-
code: str = "donna.
|
|
158
|
+
code: str = "donna.workspaces.directive_not_available"
|
|
141
159
|
message: str = "Directive `{error.module_path}.{error.directive_name}` is not available."
|
|
142
160
|
ways_to_fix: list[str] = [
|
|
143
161
|
"Check the directive name for typos.",
|
|
@@ -148,7 +166,7 @@ class DirectiveNotAvailable(TemplateDirectiveError):
|
|
|
148
166
|
|
|
149
167
|
|
|
150
168
|
class DirectiveNotDirective(TemplateDirectiveError):
|
|
151
|
-
code: str = "donna.
|
|
169
|
+
code: str = "donna.workspaces.directive_not_directive"
|
|
152
170
|
message: str = "`{error.module_path}.{error.directive_name}` is not a directive."
|
|
153
171
|
ways_to_fix: list[str] = [
|
|
154
172
|
"Check the directive path for typos.",
|
|
@@ -160,7 +178,7 @@ class DirectiveNotDirective(TemplateDirectiveError):
|
|
|
160
178
|
|
|
161
179
|
|
|
162
180
|
class DirectiveUnexpectedError(TemplateDirectiveError):
|
|
163
|
-
code: str = "donna.
|
|
181
|
+
code: str = "donna.workspaces.directive_unexpected_error"
|
|
164
182
|
message: str = "Unexpected error while applying directive `{error.directive_path}`: {error.details}"
|
|
165
183
|
ways_to_fix: list[str] = [
|
|
166
184
|
"Check the documentation for the directive to ensure correct usage.",
|
|
@@ -171,7 +189,7 @@ class DirectiveUnexpectedError(TemplateDirectiveError):
|
|
|
171
189
|
|
|
172
190
|
|
|
173
191
|
class MarkdownUnsupportedCodeFormat(MarkdownError):
|
|
174
|
-
code: str = "donna.
|
|
192
|
+
code: str = "donna.workspaces.markdown_unsupported_code_format"
|
|
175
193
|
message: str = "Unsupported code block format `{error.format}`"
|
|
176
194
|
ways_to_fix: list[str] = [
|
|
177
195
|
"Use one of the supported formats: json, yaml, yml, toml.",
|
|
@@ -180,36 +198,28 @@ class MarkdownUnsupportedCodeFormat(MarkdownError):
|
|
|
180
198
|
|
|
181
199
|
|
|
182
200
|
class MarkdownMultipleH1Sections(MarkdownError):
|
|
183
|
-
code: str = "donna.
|
|
201
|
+
code: str = "donna.workspaces.markdown_multiple_h1_sections"
|
|
184
202
|
message: str = "Multiple H1 sections are not supported"
|
|
185
203
|
ways_to_fix: list[str] = [
|
|
186
204
|
"Keep a single H1 section in the artifact.",
|
|
187
205
|
]
|
|
188
206
|
|
|
189
207
|
|
|
190
|
-
class
|
|
191
|
-
code: str = "donna.
|
|
192
|
-
message: str = "
|
|
193
|
-
ways_to_fix: list[str] = [
|
|
194
|
-
"Keep a single H1 title in the artifact.",
|
|
195
|
-
]
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
class MarkdownH2BeforeH1Title(MarkdownError):
|
|
199
|
-
code: str = "donna.world.markdown_h2_before_h1_title"
|
|
200
|
-
message: str = "H2 section found before H1 title"
|
|
208
|
+
class MarkdownH1SectionMustBeFirst(MarkdownError):
|
|
209
|
+
code: str = "donna.workspaces.markdown_h1_section_must_be_first"
|
|
210
|
+
message: str = "H1 section must be the first section in the artifact"
|
|
201
211
|
ways_to_fix: list[str] = [
|
|
202
|
-
"Ensure the
|
|
212
|
+
"Ensure the H1 section is the first section in the artifact.",
|
|
203
213
|
]
|
|
204
214
|
|
|
205
215
|
|
|
206
216
|
class MarkdownArtifactWithoutSections(MarkdownError):
|
|
207
|
-
code: str = "donna.
|
|
217
|
+
code: str = "donna.workspaces.markdown_artifact_without_sections"
|
|
208
218
|
message: str = "Artifact MUST have at least one section"
|
|
209
219
|
|
|
210
220
|
|
|
211
221
|
class PrimitiveDoesNotSupportMarkdown(MarkdownError):
|
|
212
|
-
code: str = "donna.
|
|
222
|
+
code: str = "donna.workspaces.primitive_does_not_support_markdown"
|
|
213
223
|
message: str = "Primitive {error.primitive_id} cannot construct artifact section from the Markdown source"
|
|
214
224
|
ways_to_fix: list[str] = [
|
|
215
225
|
"Ensure the section kind points to a primitive that supports Markdown sections.",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import tomllib
|
|
3
|
+
|
|
4
|
+
import tomli_w
|
|
5
|
+
|
|
6
|
+
from donna.core import errors as core_errors
|
|
7
|
+
from donna.core import utils
|
|
8
|
+
from donna.core.result import Err, Ok, Result, unwrap_to_error
|
|
9
|
+
from donna.domain.ids import WorldId
|
|
10
|
+
from donna.workspaces import config
|
|
11
|
+
from donna.workspaces import errors as world_errors
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@unwrap_to_error
|
|
15
|
+
def initialize_runtime() -> Result[None, core_errors.ErrorsList]:
|
|
16
|
+
"""Initialize the runtime environment for the application.
|
|
17
|
+
|
|
18
|
+
This function MUST be called before any other operations.
|
|
19
|
+
"""
|
|
20
|
+
project_dir = utils.discover_project_dir(config.DONNA_DIR_NAME).unwrap()
|
|
21
|
+
|
|
22
|
+
config.project_dir.set(project_dir)
|
|
23
|
+
|
|
24
|
+
config_dir = project_dir / config.DONNA_DIR_NAME
|
|
25
|
+
|
|
26
|
+
config.config_dir.set(config_dir)
|
|
27
|
+
|
|
28
|
+
config_path = config_dir / config.DONNA_CONFIG_NAME
|
|
29
|
+
|
|
30
|
+
if not config_path.exists():
|
|
31
|
+
config.config.set(config.Config())
|
|
32
|
+
return Ok(None)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
36
|
+
except tomllib.TOMLDecodeError as e:
|
|
37
|
+
return Err([world_errors.ConfigParseFailed(config_path=config_path, details=str(e))])
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
loaded_config = config.Config.model_validate(data)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return Err([world_errors.ConfigValidationFailed(config_path=config_path, details=str(e))])
|
|
43
|
+
|
|
44
|
+
config.config.set(loaded_config)
|
|
45
|
+
|
|
46
|
+
return Ok(None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@unwrap_to_error
|
|
50
|
+
def initialize_workspace(project_dir: pathlib.Path) -> Result[None, core_errors.ErrorsList]:
|
|
51
|
+
"""Initialize the physical workspace for the project (`.donna` directory)."""
|
|
52
|
+
project_dir = project_dir.resolve()
|
|
53
|
+
workspace_dir = project_dir / config.DONNA_DIR_NAME
|
|
54
|
+
|
|
55
|
+
if workspace_dir.exists():
|
|
56
|
+
return Err([world_errors.WorkspaceAlreadyInitialized(project_dir=project_dir)])
|
|
57
|
+
|
|
58
|
+
config.project_dir.set(project_dir)
|
|
59
|
+
config.config_dir.set(workspace_dir)
|
|
60
|
+
|
|
61
|
+
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
default_config = config.Config()
|
|
64
|
+
config.config.set(default_config)
|
|
65
|
+
|
|
66
|
+
config_path = workspace_dir / config.DONNA_CONFIG_NAME
|
|
67
|
+
config_path.write_text(
|
|
68
|
+
tomli_w.dumps(default_config.model_dump(mode="json")),
|
|
69
|
+
encoding="utf-8",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
project_world = default_config.get_world(WorldId(config.DONNA_WORLD_PROJECT_DIR_NAME)).unwrap()
|
|
73
|
+
project_world.initialize()
|
|
74
|
+
|
|
75
|
+
session_world = default_config.get_world(WorldId(config.DONNA_WORLD_SESSION_DIR_NAME)).unwrap()
|
|
76
|
+
session_world.initialize()
|
|
77
|
+
|
|
78
|
+
return Ok(None)
|