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,90 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
from typing import Iterable, Protocol
|
|
4
|
+
|
|
5
|
+
from donna.domain.ids import ArtifactId, FullArtifactId, FullArtifactIdPattern, WorldId
|
|
6
|
+
from donna.world.config import config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ArtifactListingNode(Protocol):
|
|
10
|
+
name: str
|
|
11
|
+
|
|
12
|
+
def is_dir(self) -> bool:
|
|
13
|
+
"""Return True when node is a directory."""
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
def is_file(self) -> bool:
|
|
17
|
+
"""Return True when node is a file."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def iterdir(self) -> Iterable["ArtifactListingNode"]:
|
|
21
|
+
"""Iterate over child nodes."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_artifacts_by_pattern( # noqa: CCR001
|
|
26
|
+
*,
|
|
27
|
+
world_id: WorldId,
|
|
28
|
+
root: ArtifactListingNode | None,
|
|
29
|
+
pattern: FullArtifactIdPattern,
|
|
30
|
+
) -> list[ArtifactId]:
|
|
31
|
+
if pattern[0] not in {"*", "**"} and pattern[0] != str(world_id):
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
if root is None or not root.is_dir():
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
pattern_parts = tuple(pattern)
|
|
38
|
+
world_prefix = (str(world_id),)
|
|
39
|
+
supported_extensions = config().supported_extensions()
|
|
40
|
+
artifacts: set[ArtifactId] = set()
|
|
41
|
+
|
|
42
|
+
def walk(node: ArtifactListingNode, parts: list[str]) -> None: # noqa: CCR001
|
|
43
|
+
for entry in sorted(node.iterdir(), key=lambda item: item.name):
|
|
44
|
+
if entry.is_dir():
|
|
45
|
+
next_parts = parts + [entry.name]
|
|
46
|
+
if not _pattern_allows_prefix(pattern_parts, world_prefix + tuple(next_parts)):
|
|
47
|
+
continue
|
|
48
|
+
walk(entry, next_parts)
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
if not entry.is_file():
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
extension = pathlib.Path(entry.name).suffix.lower()
|
|
55
|
+
if extension not in supported_extensions:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
stem = entry.name[: -len(extension)]
|
|
59
|
+
artifact_name = ":".join(parts + [stem])
|
|
60
|
+
if ArtifactId.validate(artifact_name):
|
|
61
|
+
artifact_id = ArtifactId(artifact_name)
|
|
62
|
+
full_id = FullArtifactId((world_id, artifact_id))
|
|
63
|
+
if pattern.matches_full_id(full_id):
|
|
64
|
+
artifacts.add(artifact_id)
|
|
65
|
+
|
|
66
|
+
walk(root, [])
|
|
67
|
+
|
|
68
|
+
return list(sorted(artifacts))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _pattern_allows_prefix(pattern_parts: tuple[str, ...], prefix_parts: tuple[str, ...]) -> bool:
|
|
72
|
+
@lru_cache(maxsize=None)
|
|
73
|
+
def match_at(p_index: int, v_index: int) -> bool: # noqa: CCR001
|
|
74
|
+
if v_index >= len(prefix_parts):
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
if p_index >= len(pattern_parts):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
token = pattern_parts[p_index]
|
|
81
|
+
|
|
82
|
+
if token == "**": # noqa: S105
|
|
83
|
+
return match_at(p_index + 1, v_index) or match_at(p_index, v_index + 1)
|
|
84
|
+
|
|
85
|
+
if token == "*" or token == prefix_parts[v_index]: # noqa: S105
|
|
86
|
+
return match_at(p_index + 1, v_index + 1)
|
|
87
|
+
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
return match_at(0, 0)
|
donna/world/config.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pydantic
|
|
5
|
+
|
|
6
|
+
from donna.core.entities import BaseEntity
|
|
7
|
+
from donna.core.errors import ErrorsList
|
|
8
|
+
from donna.core.result import Err, Ok, Result
|
|
9
|
+
from donna.domain.ids import PythonImportPath, WorldId
|
|
10
|
+
from donna.machine.primitives import resolve_primitive
|
|
11
|
+
from donna.world import errors as world_errors
|
|
12
|
+
from donna.world.sources.base import SourceConfig as SourceConfigValue
|
|
13
|
+
from donna.world.sources.base import SourceConstructor
|
|
14
|
+
from donna.world.worlds.base import World as BaseWorld
|
|
15
|
+
from donna.world.worlds.base import WorldConstructor
|
|
16
|
+
|
|
17
|
+
DONNA_DIR_NAME = ".donna"
|
|
18
|
+
DONNA_CONFIG_NAME = "config.toml"
|
|
19
|
+
DONNA_WORLD_SESSION_DIR_NAME = "session"
|
|
20
|
+
DONNA_WORLD_PROJECT_DIR_NAME = "project"
|
|
21
|
+
DONNA_WORLD_HOME_DIR_NAME = "home"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorldConfig(BaseEntity):
|
|
25
|
+
kind: PythonImportPath
|
|
26
|
+
id: WorldId
|
|
27
|
+
readonly: bool
|
|
28
|
+
session: bool
|
|
29
|
+
|
|
30
|
+
model_config = pydantic.ConfigDict(extra="allow")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SourceConfig(BaseEntity):
|
|
34
|
+
kind: PythonImportPath
|
|
35
|
+
|
|
36
|
+
model_config = pydantic.ConfigDict(extra="allow")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _default_sources() -> list[SourceConfig]:
|
|
40
|
+
return [
|
|
41
|
+
SourceConfig.model_validate(
|
|
42
|
+
{
|
|
43
|
+
"kind": "donna.lib.sources.markdown",
|
|
44
|
+
}
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _create_default_worlds(project_dir: pathlib.Path) -> list[WorldConfig]:
|
|
50
|
+
return [
|
|
51
|
+
WorldConfig.model_validate(
|
|
52
|
+
{
|
|
53
|
+
"id": WorldId("donna"),
|
|
54
|
+
"kind": "donna.lib.worlds.python",
|
|
55
|
+
"readonly": True,
|
|
56
|
+
"session": False,
|
|
57
|
+
"package": "donna",
|
|
58
|
+
}
|
|
59
|
+
),
|
|
60
|
+
WorldConfig.model_validate(
|
|
61
|
+
{
|
|
62
|
+
"id": WorldId("home"),
|
|
63
|
+
"kind": "donna.lib.worlds.filesystem",
|
|
64
|
+
"readonly": True,
|
|
65
|
+
"session": False,
|
|
66
|
+
"path": pathlib.Path.home() / DONNA_DIR_NAME / DONNA_WORLD_HOME_DIR_NAME,
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
WorldConfig.model_validate(
|
|
70
|
+
{
|
|
71
|
+
"id": WorldId("project"),
|
|
72
|
+
"kind": "donna.lib.worlds.filesystem",
|
|
73
|
+
"readonly": False,
|
|
74
|
+
"session": False,
|
|
75
|
+
"path": project_dir / DONNA_DIR_NAME / DONNA_WORLD_PROJECT_DIR_NAME,
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
WorldConfig.model_validate(
|
|
79
|
+
{
|
|
80
|
+
"id": WorldId("session"),
|
|
81
|
+
"kind": "donna.lib.worlds.filesystem",
|
|
82
|
+
"readonly": False,
|
|
83
|
+
"session": True,
|
|
84
|
+
"path": project_dir / DONNA_DIR_NAME / DONNA_WORLD_SESSION_DIR_NAME,
|
|
85
|
+
}
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _default_worlds() -> list[WorldConfig]:
|
|
91
|
+
return _create_default_worlds(project_dir())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Config(BaseEntity):
|
|
95
|
+
worlds: list[WorldConfig] = pydantic.Field(default_factory=_default_worlds)
|
|
96
|
+
sources: list[SourceConfig] = pydantic.Field(default_factory=_default_sources)
|
|
97
|
+
_worlds_instances: list[BaseWorld] = pydantic.PrivateAttr(default_factory=list)
|
|
98
|
+
_sources_instances: list[SourceConfigValue] = pydantic.PrivateAttr(default_factory=list)
|
|
99
|
+
|
|
100
|
+
tmp_dir: pathlib.Path = pathlib.Path("./tmp")
|
|
101
|
+
|
|
102
|
+
def model_post_init(self, __context: Any) -> None: # noqa: CCR001
|
|
103
|
+
worlds: list[BaseWorld] = []
|
|
104
|
+
sources: list[SourceConfigValue] = []
|
|
105
|
+
|
|
106
|
+
for world_config in self.worlds:
|
|
107
|
+
primitive_result = resolve_primitive(world_config.kind)
|
|
108
|
+
if primitive_result.is_err():
|
|
109
|
+
error = primitive_result.unwrap_err()[0]
|
|
110
|
+
raise ValueError(error.message.format(error=error))
|
|
111
|
+
|
|
112
|
+
primitive = primitive_result.unwrap()
|
|
113
|
+
|
|
114
|
+
if not isinstance(primitive, WorldConstructor):
|
|
115
|
+
raise ValueError(f"World constructor '{world_config.kind}' is not supported")
|
|
116
|
+
|
|
117
|
+
worlds.append(primitive.construct_world(world_config))
|
|
118
|
+
|
|
119
|
+
for source_config in self.sources:
|
|
120
|
+
primitive_result = resolve_primitive(source_config.kind)
|
|
121
|
+
if primitive_result.is_err():
|
|
122
|
+
error = primitive_result.unwrap_err()[0]
|
|
123
|
+
raise ValueError(error.message.format(error=error))
|
|
124
|
+
|
|
125
|
+
primitive = primitive_result.unwrap()
|
|
126
|
+
|
|
127
|
+
if not isinstance(primitive, SourceConstructor):
|
|
128
|
+
raise ValueError(f"Source constructor '{source_config.kind}' is not supported")
|
|
129
|
+
|
|
130
|
+
sources.append(primitive.construct_source(source_config))
|
|
131
|
+
|
|
132
|
+
object.__setattr__(self, "_worlds_instances", worlds)
|
|
133
|
+
object.__setattr__(self, "_sources_instances", sources)
|
|
134
|
+
|
|
135
|
+
def get_world(self, world_id: WorldId) -> Result[BaseWorld, ErrorsList]:
|
|
136
|
+
for world in self._worlds_instances:
|
|
137
|
+
if world.id == world_id:
|
|
138
|
+
return Ok(world)
|
|
139
|
+
|
|
140
|
+
return Err([world_errors.WorldNotConfigured(world_id=world_id)])
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def worlds_instances(self) -> list[BaseWorld]:
|
|
144
|
+
return list(self._worlds_instances)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def sources_instances(self) -> list[SourceConfigValue]:
|
|
148
|
+
return list(self._sources_instances)
|
|
149
|
+
|
|
150
|
+
def get_source_config(self, kind: str) -> Result[SourceConfigValue, ErrorsList]:
|
|
151
|
+
for source in self._sources_instances:
|
|
152
|
+
if source.kind == kind:
|
|
153
|
+
return Ok(source)
|
|
154
|
+
|
|
155
|
+
return Err([world_errors.SourceConfigNotConfigured(kind=kind)])
|
|
156
|
+
|
|
157
|
+
def find_source_for_extension(self, extension: str) -> SourceConfigValue | None:
|
|
158
|
+
for source in self._sources_instances:
|
|
159
|
+
if source.supports_extension(extension):
|
|
160
|
+
return source
|
|
161
|
+
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def supported_extensions(self) -> set[str]:
|
|
165
|
+
extensions: set[str] = set()
|
|
166
|
+
|
|
167
|
+
for source in self._sources_instances:
|
|
168
|
+
for extension in source.supported_extensions:
|
|
169
|
+
extensions.add(extension)
|
|
170
|
+
|
|
171
|
+
return extensions
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class GlobalConfig[V]():
|
|
175
|
+
__slots__ = ("_value",)
|
|
176
|
+
|
|
177
|
+
def __init__(self) -> None:
|
|
178
|
+
self._value: V | None = None
|
|
179
|
+
|
|
180
|
+
def set(self, value: V) -> None:
|
|
181
|
+
if self._value is not None:
|
|
182
|
+
raise world_errors.GlobalConfigAlreadySet()
|
|
183
|
+
|
|
184
|
+
self._value = value
|
|
185
|
+
|
|
186
|
+
def get(self) -> V:
|
|
187
|
+
if self._value is None:
|
|
188
|
+
raise world_errors.GlobalConfigNotSet()
|
|
189
|
+
|
|
190
|
+
return self._value
|
|
191
|
+
|
|
192
|
+
def __call__(self) -> V:
|
|
193
|
+
return self.get()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
project_dir = GlobalConfig[pathlib.Path]()
|
|
197
|
+
config_dir = GlobalConfig[pathlib.Path]()
|
|
198
|
+
config = GlobalConfig[Config]()
|
donna/world/errors.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
from donna.core import errors as core_errors
|
|
4
|
+
from donna.domain.ids import ArtifactId, FullArtifactId, WorldId
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InternalError(core_errors.InternalError):
|
|
8
|
+
"""Base class for internal errors in donna.world."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WorldError(core_errors.EnvironmentError):
|
|
12
|
+
cell_kind: str = "world_error"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WorldConfigError(WorldError):
|
|
16
|
+
cell_kind: str = "world_config_error"
|
|
17
|
+
config_path: pathlib.Path
|
|
18
|
+
|
|
19
|
+
def content_intro(self) -> str:
|
|
20
|
+
return f"Error in world config file '{self.config_path}'"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfigParseFailed(WorldConfigError):
|
|
24
|
+
code: str = "donna.world.config_parse_failed"
|
|
25
|
+
message: str = "Failed to parse config file: {error.details}"
|
|
26
|
+
details: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConfigValidationFailed(WorldConfigError):
|
|
30
|
+
code: str = "donna.world.config_validation_failed"
|
|
31
|
+
message: str = "Failed to validate config file: {error.details}"
|
|
32
|
+
details: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WorldNotConfigured(WorldError):
|
|
36
|
+
code: str = "donna.world.world_not_configured"
|
|
37
|
+
message: str = "World with id `{error.world_id}` is not configured"
|
|
38
|
+
world_id: WorldId
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SourceConfigNotConfigured(WorldError):
|
|
42
|
+
code: str = "donna.world.source_config_not_configured"
|
|
43
|
+
message: str = "Source config `{error.kind}` is not configured"
|
|
44
|
+
kind: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WorldReadonly(WorldError):
|
|
48
|
+
code: str = "donna.world.world_readonly"
|
|
49
|
+
message: str = "World `{error.world_id}` is read-only"
|
|
50
|
+
ways_to_fix: list[str] = [
|
|
51
|
+
"Use a world configured with readonly = false. Most likely they are `project` and `session`.",
|
|
52
|
+
]
|
|
53
|
+
world_id: WorldId
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WorldStateStorageUnsupported(WorldError):
|
|
57
|
+
code: str = "donna.world.state_storage_unsupported"
|
|
58
|
+
message: str = "World `{error.world_id}` does not support state storage"
|
|
59
|
+
ways_to_fix: list[str] = [
|
|
60
|
+
"Use the session world.",
|
|
61
|
+
]
|
|
62
|
+
world_id: WorldId
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ArtifactError(WorldError):
|
|
66
|
+
cell_kind: str = "artifact_error"
|
|
67
|
+
artifact_id: ArtifactId
|
|
68
|
+
world_id: WorldId
|
|
69
|
+
|
|
70
|
+
def content_intro(self) -> str:
|
|
71
|
+
return f"Error for artifact '{self.artifact_id}' in world '{self.world_id}'"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ArtifactNotFound(ArtifactError):
|
|
75
|
+
code: str = "donna.world.artifact_not_found"
|
|
76
|
+
message: str = "Artifact `{error.artifact_id}` does not exist in world `{error.world_id}`"
|
|
77
|
+
ways_to_fix: list[str] = [
|
|
78
|
+
"Check the artifact id for typos.",
|
|
79
|
+
"Ensure the artifact exists in the specified world.",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ArtifactMultipleFiles(ArtifactError):
|
|
84
|
+
code: str = "donna.world.artifact_multiple_files"
|
|
85
|
+
message: str = "Artifact `{error.artifact_id}` has multiple files in world `{error.world_id}`"
|
|
86
|
+
ways_to_fix: list[str] = [
|
|
87
|
+
"Keep a single source file per artifact in the world.",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class UnsupportedArtifactSourceExtension(ArtifactError):
|
|
92
|
+
code: str = "donna.world.unsupported_artifact_source_extension"
|
|
93
|
+
message: str = "Unsupported artifact source extension `{error.extension}` in world `{error.world_id}`"
|
|
94
|
+
ways_to_fix: list[str] = [
|
|
95
|
+
"Use a supported extension for the configured sources.",
|
|
96
|
+
]
|
|
97
|
+
extension: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MarkdownError(WorldError):
|
|
101
|
+
cell_kind: str = "markdown_error"
|
|
102
|
+
artifact_id: FullArtifactId | None = None
|
|
103
|
+
|
|
104
|
+
def content_intro(self) -> str:
|
|
105
|
+
if self.artifact_id is None:
|
|
106
|
+
return "Error in markdown source"
|
|
107
|
+
|
|
108
|
+
return f"Error in markdown artifact '{self.artifact_id}'"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TemplateDirectiveError(WorldError):
|
|
112
|
+
cell_kind: str = "template_directive_error"
|
|
113
|
+
artifact_id: FullArtifactId | None = None
|
|
114
|
+
|
|
115
|
+
def content_intro(self) -> str:
|
|
116
|
+
if self.artifact_id is None:
|
|
117
|
+
return "Error in template directive"
|
|
118
|
+
|
|
119
|
+
return f"Error in template directive for artifact '{self.artifact_id}'"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class DirectivePathIncomplete(TemplateDirectiveError):
|
|
123
|
+
code: str = "donna.world.directive_path_incomplete"
|
|
124
|
+
message: str = "Directive path must include module and directive parts, got `{error.path}`."
|
|
125
|
+
ways_to_fix: list[str] = ["Use a directive path with both module and directive names."]
|
|
126
|
+
path: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class DirectiveModuleNotImportable(TemplateDirectiveError):
|
|
130
|
+
code: str = "donna.world.directive_module_not_importable"
|
|
131
|
+
message: str = "Directive module `{error.module_path}` is not importable."
|
|
132
|
+
ways_to_fix: list[str] = [
|
|
133
|
+
"Check the module path for typos.",
|
|
134
|
+
"Ensure the module exists and is importable in the current environment.",
|
|
135
|
+
]
|
|
136
|
+
module_path: str
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DirectiveNotAvailable(TemplateDirectiveError):
|
|
140
|
+
code: str = "donna.world.directive_not_available"
|
|
141
|
+
message: str = "Directive `{error.module_path}.{error.directive_name}` is not available."
|
|
142
|
+
ways_to_fix: list[str] = [
|
|
143
|
+
"Check the directive name for typos.",
|
|
144
|
+
"Ensure the directive is defined in the module.",
|
|
145
|
+
]
|
|
146
|
+
module_path: str
|
|
147
|
+
directive_name: str
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DirectiveNotDirective(TemplateDirectiveError):
|
|
151
|
+
code: str = "donna.world.directive_not_directive"
|
|
152
|
+
message: str = "`{error.module_path}.{error.directive_name}` is not a directive."
|
|
153
|
+
ways_to_fix: list[str] = [
|
|
154
|
+
"Check the directive path for typos.",
|
|
155
|
+
"Check if you use the right primitive for the task.",
|
|
156
|
+
"Ensure the referenced object is a `donna.machine.templates.Directive` instance.",
|
|
157
|
+
]
|
|
158
|
+
module_path: str
|
|
159
|
+
directive_name: str
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class DirectiveUnexpectedError(TemplateDirectiveError):
|
|
163
|
+
code: str = "donna.world.directive_unexpected_error"
|
|
164
|
+
message: str = "Unexpected error while applying directive `{error.directive_path}`: {error.details}"
|
|
165
|
+
ways_to_fix: list[str] = [
|
|
166
|
+
"Check the documentation for the directive to ensure correct usage.",
|
|
167
|
+
"Ask the developer to help debug the issue.",
|
|
168
|
+
]
|
|
169
|
+
directive_path: str
|
|
170
|
+
details: str
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class MarkdownUnsupportedCodeFormat(MarkdownError):
|
|
174
|
+
code: str = "donna.world.markdown_unsupported_code_format"
|
|
175
|
+
message: str = "Unsupported code block format `{error.format}`"
|
|
176
|
+
ways_to_fix: list[str] = [
|
|
177
|
+
"Use one of the supported formats: json, yaml, yml, toml.",
|
|
178
|
+
]
|
|
179
|
+
format: str
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class MarkdownMultipleH1Sections(MarkdownError):
|
|
183
|
+
code: str = "donna.world.markdown_multiple_h1_sections"
|
|
184
|
+
message: str = "Multiple H1 sections are not supported"
|
|
185
|
+
ways_to_fix: list[str] = [
|
|
186
|
+
"Keep a single H1 section in the artifact.",
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class MarkdownMultipleH1Titles(MarkdownError):
|
|
191
|
+
code: str = "donna.world.markdown_multiple_h1_titles"
|
|
192
|
+
message: str = "Multiple H1 titles are not supported"
|
|
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"
|
|
201
|
+
ways_to_fix: list[str] = [
|
|
202
|
+
"Ensure the first heading is an H1 title before any H2 sections.",
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class MarkdownArtifactWithoutSections(MarkdownError):
|
|
207
|
+
code: str = "donna.world.markdown_artifact_without_sections"
|
|
208
|
+
message: str = "Artifact MUST have at least one section"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class PrimitiveDoesNotSupportMarkdown(MarkdownError):
|
|
212
|
+
code: str = "donna.world.primitive_does_not_support_markdown"
|
|
213
|
+
message: str = "Primitive {error.primitive_id} cannot construct artifact section from the Markdown source"
|
|
214
|
+
ways_to_fix: list[str] = [
|
|
215
|
+
"Ensure the section kind points to a primitive that supports Markdown sections.",
|
|
216
|
+
]
|
|
217
|
+
primitive_id: str
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class MarkdownSectionsCountMismatch(InternalError):
|
|
221
|
+
message = (
|
|
222
|
+
"Artifact `{artifact_id}` has {original_count} sections in the original render "
|
|
223
|
+
"and {analyzed_count} sections in the analysis render."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class GlobalConfigAlreadySet(InternalError):
|
|
228
|
+
message = "Global config value is already set"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class GlobalConfigNotSet(InternalError):
|
|
232
|
+
message = "Global config value is not set"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
|
|
3
|
+
from donna.core import errors as core_errors
|
|
4
|
+
from donna.core import utils
|
|
5
|
+
from donna.core.result import Err, Ok, Result, unwrap_to_error
|
|
6
|
+
from donna.world import config
|
|
7
|
+
from donna.world import errors as world_errors
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@unwrap_to_error
|
|
11
|
+
def initialize_environment() -> Result[None, core_errors.ErrorsList]:
|
|
12
|
+
"""Initialize the environment for the application.
|
|
13
|
+
|
|
14
|
+
This function MUST be called before any other operations.
|
|
15
|
+
"""
|
|
16
|
+
project_dir = utils.discover_project_dir(config.DONNA_DIR_NAME).unwrap()
|
|
17
|
+
|
|
18
|
+
config.project_dir.set(project_dir)
|
|
19
|
+
|
|
20
|
+
config_dir = project_dir / config.DONNA_DIR_NAME
|
|
21
|
+
|
|
22
|
+
config.config_dir.set(config_dir)
|
|
23
|
+
|
|
24
|
+
config_path = config_dir / config.DONNA_CONFIG_NAME
|
|
25
|
+
|
|
26
|
+
if not config_path.exists():
|
|
27
|
+
config.config.set(config.Config())
|
|
28
|
+
return Ok(None)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
data = tomllib.loads(config_path.read_text())
|
|
32
|
+
except tomllib.TOMLDecodeError as e:
|
|
33
|
+
return Err([world_errors.ConfigParseFailed(config_path=config_path, details=str(e))])
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
loaded_config = config.Config.model_validate(data)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return Err([world_errors.ConfigValidationFailed(config_path=config_path, details=str(e))])
|
|
39
|
+
|
|
40
|
+
config.config.set(loaded_config)
|
|
41
|
+
|
|
42
|
+
return Ok(None)
|