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.
Files changed (85) hide show
  1. donna/__init__.py +1 -0
  2. donna/artifacts/__init__.py +0 -0
  3. donna/artifacts/usage/__init__.py +0 -0
  4. donna/artifacts/usage/artifacts.md +224 -0
  5. donna/artifacts/usage/cli.md +117 -0
  6. donna/artifacts/usage/worlds.md +36 -0
  7. donna/artifacts/work/__init__.py +0 -0
  8. donna/artifacts/work/do_it.md +142 -0
  9. donna/artifacts/work/do_it_fast.md +98 -0
  10. donna/artifacts/work/planning.md +245 -0
  11. donna/cli/__init__.py +0 -0
  12. donna/cli/__main__.py +6 -0
  13. donna/cli/application.py +17 -0
  14. donna/cli/commands/__init__.py +0 -0
  15. donna/cli/commands/artifacts.py +110 -0
  16. donna/cli/commands/projects.py +49 -0
  17. donna/cli/commands/sessions.py +77 -0
  18. donna/cli/types.py +138 -0
  19. donna/cli/utils.py +53 -0
  20. donna/core/__init__.py +0 -0
  21. donna/core/entities.py +27 -0
  22. donna/core/errors.py +126 -0
  23. donna/core/result.py +99 -0
  24. donna/core/utils.py +37 -0
  25. donna/domain/__init__.py +0 -0
  26. donna/domain/errors.py +47 -0
  27. donna/domain/ids.py +497 -0
  28. donna/lib/__init__.py +21 -0
  29. donna/lib/sources.py +5 -0
  30. donna/lib/worlds.py +7 -0
  31. donna/machine/__init__.py +0 -0
  32. donna/machine/action_requests.py +50 -0
  33. donna/machine/artifacts.py +200 -0
  34. donna/machine/changes.py +91 -0
  35. donna/machine/errors.py +122 -0
  36. donna/machine/operations.py +31 -0
  37. donna/machine/primitives.py +77 -0
  38. donna/machine/sessions.py +215 -0
  39. donna/machine/state.py +244 -0
  40. donna/machine/tasks.py +89 -0
  41. donna/machine/templates.py +83 -0
  42. donna/primitives/__init__.py +1 -0
  43. donna/primitives/artifacts/__init__.py +0 -0
  44. donna/primitives/artifacts/specification.py +20 -0
  45. donna/primitives/artifacts/workflow.py +195 -0
  46. donna/primitives/directives/__init__.py +0 -0
  47. donna/primitives/directives/goto.py +44 -0
  48. donna/primitives/directives/task_variable.py +73 -0
  49. donna/primitives/directives/view.py +45 -0
  50. donna/primitives/operations/__init__.py +0 -0
  51. donna/primitives/operations/finish_workflow.py +37 -0
  52. donna/primitives/operations/request_action.py +89 -0
  53. donna/primitives/operations/run_script.py +250 -0
  54. donna/protocol/__init__.py +0 -0
  55. donna/protocol/cell_shortcuts.py +9 -0
  56. donna/protocol/cells.py +44 -0
  57. donna/protocol/errors.py +17 -0
  58. donna/protocol/formatters/__init__.py +0 -0
  59. donna/protocol/formatters/automation.py +25 -0
  60. donna/protocol/formatters/base.py +15 -0
  61. donna/protocol/formatters/human.py +36 -0
  62. donna/protocol/formatters/llm.py +39 -0
  63. donna/protocol/modes.py +40 -0
  64. donna/protocol/nodes.py +59 -0
  65. donna/world/__init__.py +0 -0
  66. donna/world/artifacts.py +122 -0
  67. donna/world/artifacts_discovery.py +90 -0
  68. donna/world/config.py +198 -0
  69. donna/world/errors.py +232 -0
  70. donna/world/initialization.py +42 -0
  71. donna/world/markdown.py +267 -0
  72. donna/world/sources/__init__.py +1 -0
  73. donna/world/sources/base.py +62 -0
  74. donna/world/sources/markdown.py +260 -0
  75. donna/world/templates.py +181 -0
  76. donna/world/tmp.py +33 -0
  77. donna/world/worlds/__init__.py +0 -0
  78. donna/world/worlds/base.py +68 -0
  79. donna/world/worlds/filesystem.py +189 -0
  80. donna/world/worlds/python.py +196 -0
  81. donna-0.2.0.dist-info/METADATA +44 -0
  82. donna-0.2.0.dist-info/RECORD +85 -0
  83. donna-0.2.0.dist-info/WHEEL +4 -0
  84. donna-0.2.0.dist-info/entry_points.txt +3 -0
  85. 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)