donna 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- donna/artifacts/intro.md +39 -0
- donna/artifacts/research/specs/report.md +163 -0
- donna/artifacts/research/work/research.md +198 -0
- donna/artifacts/rfc/specs/request_for_change.md +271 -0
- donna/artifacts/rfc/work/create.md +120 -0
- donna/artifacts/rfc/work/do.md +109 -0
- donna/artifacts/rfc/work/plan.md +68 -0
- donna/artifacts/usage/artifacts.md +41 -6
- donna/artifacts/usage/cli.md +106 -37
- donna/artifacts/usage/worlds.md +8 -2
- donna/cli/__main__.py +1 -1
- donna/cli/commands/artifacts.py +104 -17
- donna/cli/commands/sessions.py +7 -7
- donna/cli/commands/workspaces.py +42 -0
- donna/cli/errors.py +18 -0
- donna/cli/types.py +16 -9
- donna/cli/utils.py +2 -2
- donna/core/errors.py +1 -11
- donna/core/result.py +5 -8
- donna/core/utils.py +0 -3
- donna/lib/__init__.py +4 -0
- donna/lib/sources.py +1 -1
- donna/lib/worlds.py +2 -2
- donna/machine/action_requests.py +0 -5
- donna/machine/artifacts.py +8 -6
- donna/machine/primitives.py +4 -4
- donna/machine/sessions.py +12 -4
- donna/machine/state.py +2 -2
- donna/machine/tasks.py +4 -18
- donna/machine/templates.py +4 -2
- donna/primitives/artifacts/specification.py +13 -2
- donna/primitives/artifacts/workflow.py +11 -2
- donna/primitives/directives/list.py +86 -0
- donna/primitives/directives/view.py +52 -11
- donna/primitives/operations/finish_workflow.py +13 -2
- donna/primitives/operations/output.py +87 -0
- donna/primitives/operations/request_action.py +3 -9
- donna/primitives/operations/run_script.py +2 -2
- donna/protocol/utils.py +22 -0
- donna/workspaces/artifacts.py +238 -0
- donna/{world → workspaces}/artifacts_discovery.py +1 -1
- donna/{world → workspaces}/config.py +13 -6
- donna/{world → workspaces}/errors.py +55 -45
- donna/workspaces/initialization.py +78 -0
- donna/{world → workspaces}/markdown.py +21 -26
- donna/{world → workspaces}/sources/base.py +2 -2
- donna/{world → workspaces}/sources/markdown.py +7 -6
- donna/{world → workspaces}/templates.py +4 -4
- donna/{world → workspaces}/tmp.py +19 -1
- donna/{world → workspaces}/worlds/base.py +5 -2
- donna/{world → workspaces}/worlds/filesystem.py +23 -9
- donna/{world → workspaces}/worlds/python.py +12 -9
- {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/METADATA +4 -1
- donna-0.2.1.dist-info/RECORD +92 -0
- {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/WHEEL +1 -1
- donna/artifacts/work/do_it.md +0 -142
- donna/artifacts/work/do_it_fast.md +0 -98
- donna/artifacts/work/planning.md +0 -245
- donna/cli/commands/projects.py +0 -49
- donna/world/artifacts.py +0 -122
- donna/world/initialization.py +0 -42
- donna/world/worlds/__init__.py +0 -0
- donna-0.2.0.dist-info/RECORD +0 -85
- /donna/{artifacts/work → workspaces}/__init__.py +0 -0
- /donna/{world → workspaces}/sources/__init__.py +0 -0
- /donna/{world → workspaces/worlds}/__init__.py +0 -0
- {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/entry_points.txt +0 -0
- {donna-0.2.0.dist-info → donna-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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"
|
|
@@ -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)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import enum
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
-
import pydantic
|
|
5
4
|
from markdown_it import MarkdownIt
|
|
6
5
|
from markdown_it.token import Token
|
|
7
6
|
from markdown_it.tree import SyntaxTreeNode
|
|
@@ -11,7 +10,7 @@ from donna.core.entities import BaseEntity
|
|
|
11
10
|
from donna.core.errors import ErrorsList
|
|
12
11
|
from donna.core.result import Err, Ok, Result, unwrap_to_error
|
|
13
12
|
from donna.domain.ids import FullArtifactId
|
|
14
|
-
from donna.
|
|
13
|
+
from donna.workspaces import errors as world_errors
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class SectionLevel(str, enum.Enum):
|
|
@@ -54,8 +53,6 @@ class SectionSource(BaseEntity):
|
|
|
54
53
|
original_tokens: list[Token]
|
|
55
54
|
analysis_tokens: list[Token]
|
|
56
55
|
|
|
57
|
-
model_config = pydantic.ConfigDict(frozen=False)
|
|
58
|
-
|
|
59
56
|
def _as_markdown(self, tokens: list[Token], with_title: bool) -> str:
|
|
60
57
|
parts = []
|
|
61
58
|
|
|
@@ -110,15 +107,21 @@ def clear_heading(text: str) -> str:
|
|
|
110
107
|
def _parse_h1(
|
|
111
108
|
sections: list[SectionSource], node: SyntaxTreeNode, artifact_id: FullArtifactId | None
|
|
112
109
|
) -> Result[SyntaxTreeNode | None, ErrorsList]:
|
|
113
|
-
section
|
|
114
|
-
|
|
115
|
-
if section.level != SectionLevel.h1:
|
|
110
|
+
if sections and any(section.level == SectionLevel.h1 for section in sections):
|
|
116
111
|
return Err([world_errors.MarkdownMultipleH1Sections(artifact_id=artifact_id)])
|
|
117
112
|
|
|
118
|
-
if
|
|
119
|
-
return Err([world_errors.
|
|
113
|
+
if sections:
|
|
114
|
+
return Err([world_errors.MarkdownH1SectionMustBeFirst(artifact_id=artifact_id)])
|
|
115
|
+
|
|
116
|
+
new_section = SectionSource(
|
|
117
|
+
level=SectionLevel.h1,
|
|
118
|
+
title=clear_heading(render_back(node.to_tokens()).strip()),
|
|
119
|
+
original_tokens=[],
|
|
120
|
+
analysis_tokens=[],
|
|
121
|
+
configs=[],
|
|
122
|
+
)
|
|
120
123
|
|
|
121
|
-
|
|
124
|
+
sections.append(new_section)
|
|
122
125
|
|
|
123
126
|
return Ok(node.next_sibling)
|
|
124
127
|
|
|
@@ -126,10 +129,9 @@ def _parse_h1(
|
|
|
126
129
|
def _parse_h2(
|
|
127
130
|
sections: list[SectionSource], node: SyntaxTreeNode, artifact_id: FullArtifactId | None
|
|
128
131
|
) -> Result[SyntaxTreeNode | None, ErrorsList]:
|
|
129
|
-
section = sections[-1]
|
|
130
132
|
|
|
131
|
-
if
|
|
132
|
-
return Err([world_errors.
|
|
133
|
+
if not sections:
|
|
134
|
+
return Err([world_errors.MarkdownH1SectionMustBeFirst(artifact_id=artifact_id)])
|
|
133
135
|
|
|
134
136
|
new_section = SectionSource(
|
|
135
137
|
level=SectionLevel.h2,
|
|
@@ -147,7 +149,6 @@ def _parse_h2(
|
|
|
147
149
|
def _parse_heading(
|
|
148
150
|
sections: list[SectionSource], node: SyntaxTreeNode, artifact_id: FullArtifactId | None
|
|
149
151
|
) -> Result[SyntaxTreeNode | None, ErrorsList]:
|
|
150
|
-
section = sections[-1]
|
|
151
152
|
|
|
152
153
|
if node.tag == "h1":
|
|
153
154
|
return _parse_h1(sections, node, artifact_id)
|
|
@@ -155,7 +156,11 @@ def _parse_heading(
|
|
|
155
156
|
if node.tag == "h2":
|
|
156
157
|
return _parse_h2(sections, node, artifact_id)
|
|
157
158
|
|
|
158
|
-
|
|
159
|
+
if not sections:
|
|
160
|
+
return Err([world_errors.MarkdownH1SectionMustBeFirst(artifact_id=artifact_id)])
|
|
161
|
+
|
|
162
|
+
sections[-1].original_tokens.extend(node.to_tokens())
|
|
163
|
+
|
|
159
164
|
return Ok(node.next_sibling)
|
|
160
165
|
|
|
161
166
|
|
|
@@ -231,15 +236,7 @@ def parse( # noqa: CCR001, CFQ001
|
|
|
231
236
|
# we do not need root node
|
|
232
237
|
node: SyntaxTreeNode | None = SyntaxTreeNode(tokens).children[0]
|
|
233
238
|
|
|
234
|
-
sections: list[SectionSource] = [
|
|
235
|
-
SectionSource(
|
|
236
|
-
level=SectionLevel.h1,
|
|
237
|
-
title=None,
|
|
238
|
-
original_tokens=[],
|
|
239
|
-
analysis_tokens=[],
|
|
240
|
-
configs=[],
|
|
241
|
-
)
|
|
242
|
-
]
|
|
239
|
+
sections: list[SectionSource] = []
|
|
243
240
|
|
|
244
241
|
while node is not None:
|
|
245
242
|
|
|
@@ -263,5 +260,3 @@ def parse( # noqa: CCR001, CFQ001
|
|
|
263
260
|
node = node.next_sibling
|
|
264
261
|
|
|
265
262
|
return Ok(sections)
|
|
266
|
-
|
|
267
|
-
return sections
|
|
@@ -13,8 +13,8 @@ from donna.machine.primitives import Primitive
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from donna.domain.ids import FullArtifactId
|
|
15
15
|
from donna.machine.artifacts import Artifact
|
|
16
|
-
from donna.
|
|
17
|
-
from donna.
|
|
16
|
+
from donna.workspaces.artifacts import ArtifactRenderContext
|
|
17
|
+
from donna.workspaces.config import SourceConfig as SourceConfigModel
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class SourceConfig(BaseEntity, ABC):
|
|
@@ -6,11 +6,11 @@ from donna.core.result import Err, Ok, Result, unwrap_to_error
|
|
|
6
6
|
from donna.domain.ids import ArtifactSectionId, FullArtifactId, PythonImportPath
|
|
7
7
|
from donna.machine.artifacts import Artifact, ArtifactSection, ArtifactSectionConfig, ArtifactSectionMeta
|
|
8
8
|
from donna.machine.primitives import Primitive, resolve_primitive
|
|
9
|
-
from donna.
|
|
10
|
-
from donna.
|
|
11
|
-
from donna.
|
|
12
|
-
from donna.
|
|
13
|
-
from donna.
|
|
9
|
+
from donna.workspaces import errors as world_errors
|
|
10
|
+
from donna.workspaces import markdown
|
|
11
|
+
from donna.workspaces.artifacts import ArtifactRenderContext
|
|
12
|
+
from donna.workspaces.sources.base import SourceConfig, SourceConstructor
|
|
13
|
+
from donna.workspaces.templates import RenderMode, render
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class MarkdownSectionConstructor(Protocol):
|
|
@@ -111,6 +111,7 @@ class MarkdownSectionMixin:
|
|
|
111
111
|
kind=section_config.kind,
|
|
112
112
|
title=title,
|
|
113
113
|
description=description,
|
|
114
|
+
tags=section_config.tags,
|
|
114
115
|
primary=primary,
|
|
115
116
|
meta=meta,
|
|
116
117
|
)
|
|
@@ -257,4 +258,4 @@ def _ensure_markdown_constructible(
|
|
|
257
258
|
|
|
258
259
|
|
|
259
260
|
if TYPE_CHECKING:
|
|
260
|
-
from donna.
|
|
261
|
+
from donna.workspaces.config import SourceConfig as SourceConfigModel
|
|
@@ -12,10 +12,10 @@ from donna.core.errors import EnvironmentErrorsProxy, ErrorsList
|
|
|
12
12
|
from donna.core.result import Err, Ok, Result
|
|
13
13
|
from donna.domain.ids import FullArtifactId
|
|
14
14
|
from donna.machine.templates import Directive
|
|
15
|
-
from donna.
|
|
15
|
+
from donna.workspaces import errors as world_errors
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
-
from donna.
|
|
18
|
+
from donna.workspaces.artifacts import ArtifactRenderContext
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class RenderMode(enum.Enum):
|
|
@@ -57,7 +57,7 @@ class DirectivePathBuilder:
|
|
|
57
57
|
return DirectivePathBuilder(self._parts + (name,))
|
|
58
58
|
|
|
59
59
|
@jinja2.pass_context
|
|
60
|
-
def __call__(self, context: jinja2.runtime.Context, *argv: object) -> object: # noqa: CCR001
|
|
60
|
+
def __call__(self, context: jinja2.runtime.Context, *argv: object, **kwargs: object) -> object: # noqa: CCR001
|
|
61
61
|
artifact_id = context.get("artifact_id")
|
|
62
62
|
directive_path = ".".join(self._parts)
|
|
63
63
|
if len(self._parts) < 2:
|
|
@@ -112,7 +112,7 @@ class DirectivePathBuilder:
|
|
|
112
112
|
)
|
|
113
113
|
|
|
114
114
|
try:
|
|
115
|
-
result = directive.apply_directive(context, *argv)
|
|
115
|
+
result = directive.apply_directive(context, *argv, **kwargs)
|
|
116
116
|
except EnvironmentErrorsProxy:
|
|
117
117
|
raise
|
|
118
118
|
except core_errors.InternalError:
|
|
@@ -3,7 +3,7 @@ import shutil
|
|
|
3
3
|
import time
|
|
4
4
|
|
|
5
5
|
from donna.cli.types import FullArtifactIdArgument
|
|
6
|
-
from donna.
|
|
6
|
+
from donna.workspaces.config import config, config_dir
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def dir() -> pathlib.Path:
|
|
@@ -29,5 +29,23 @@ def file_for_artifact(artifact_id: FullArtifactIdArgument, extention: str) -> pa
|
|
|
29
29
|
return directory / artifact_file_name
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def file_for_slug(slug: str, extension: str) -> pathlib.Path:
|
|
33
|
+
directory = dir()
|
|
34
|
+
|
|
35
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
normalized_slug = slug.replace("/", ".").replace("\\", ".")
|
|
38
|
+
normalized_extension = extension.lstrip(".")
|
|
39
|
+
artifact_file_name = f"{normalized_slug}.{int(time.time() * 1000)}.{normalized_extension}"
|
|
40
|
+
|
|
41
|
+
return directory / artifact_file_name
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_file_for_slug(slug: str, extension: str) -> pathlib.Path:
|
|
45
|
+
path = file_for_slug(slug, extension)
|
|
46
|
+
path.touch()
|
|
47
|
+
return path
|
|
48
|
+
|
|
49
|
+
|
|
32
50
|
def clear() -> None:
|
|
33
51
|
shutil.rmtree(dir())
|
|
@@ -11,8 +11,8 @@ from donna.machine.artifacts import Artifact
|
|
|
11
11
|
from donna.machine.primitives import Primitive
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from donna.
|
|
15
|
-
from donna.
|
|
14
|
+
from donna.workspaces.artifacts import ArtifactRenderContext
|
|
15
|
+
from donna.workspaces.config import WorldConfig
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class World(BaseEntity, ABC):
|
|
@@ -36,6 +36,9 @@ class World(BaseEntity, ABC):
|
|
|
36
36
|
self, artifact_id: ArtifactId, content: bytes, extension: str
|
|
37
37
|
) -> Result[None, ErrorsList]: ... # noqa: E704
|
|
38
38
|
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def remove(self, artifact_id: ArtifactId) -> Result[None, ErrorsList]: ... # noqa: E704
|
|
41
|
+
|
|
39
42
|
@abstractmethod
|
|
40
43
|
def file_extension_for(self, artifact_id: ArtifactId) -> Result[str, ErrorsList]: ... # noqa: E704
|
|
41
44
|
|
|
@@ -6,14 +6,14 @@ from donna.core.errors import ErrorsList
|
|
|
6
6
|
from donna.core.result import Err, Ok, Result, unwrap_to_error
|
|
7
7
|
from donna.domain.ids import ArtifactId, FullArtifactId, FullArtifactIdPattern
|
|
8
8
|
from donna.machine.artifacts import Artifact
|
|
9
|
-
from donna.
|
|
10
|
-
from donna.
|
|
11
|
-
from donna.
|
|
12
|
-
from donna.
|
|
13
|
-
from donna.
|
|
9
|
+
from donna.workspaces import errors as world_errors
|
|
10
|
+
from donna.workspaces.artifacts import ArtifactRenderContext
|
|
11
|
+
from donna.workspaces.artifacts_discovery import ArtifactListingNode, list_artifacts_by_pattern
|
|
12
|
+
from donna.workspaces.worlds.base import World as BaseWorld
|
|
13
|
+
from donna.workspaces.worlds.base import WorldConstructor
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
|
-
from donna.
|
|
16
|
+
from donna.workspaces.config import SourceConfigValue, WorldConfig
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class World(BaseWorld):
|
|
@@ -35,7 +35,7 @@ class World(BaseWorld):
|
|
|
35
35
|
if not parent.exists():
|
|
36
36
|
return Ok(None)
|
|
37
37
|
|
|
38
|
-
from donna.
|
|
38
|
+
from donna.workspaces.config import config
|
|
39
39
|
|
|
40
40
|
supported_extensions = config().supported_extensions()
|
|
41
41
|
matches = [
|
|
@@ -55,7 +55,7 @@ class World(BaseWorld):
|
|
|
55
55
|
def _get_source_by_filename(
|
|
56
56
|
self, artifact_id: ArtifactId, filename: str
|
|
57
57
|
) -> Result["SourceConfigValue", ErrorsList]:
|
|
58
|
-
from donna.
|
|
58
|
+
from donna.workspaces.config import config
|
|
59
59
|
|
|
60
60
|
extension = pathlib.Path(filename).suffix
|
|
61
61
|
source_config = config().find_source_for_extension(extension)
|
|
@@ -89,7 +89,7 @@ class World(BaseWorld):
|
|
|
89
89
|
full_id = FullArtifactId((self.id, artifact_id))
|
|
90
90
|
|
|
91
91
|
extension = pathlib.Path(path.name).suffix
|
|
92
|
-
from donna.
|
|
92
|
+
from donna.workspaces.config import config
|
|
93
93
|
|
|
94
94
|
source_config = config().find_source_for_extension(extension)
|
|
95
95
|
if source_config is None:
|
|
@@ -123,6 +123,20 @@ class World(BaseWorld):
|
|
|
123
123
|
path.write_bytes(content)
|
|
124
124
|
return Ok(None)
|
|
125
125
|
|
|
126
|
+
@unwrap_to_error
|
|
127
|
+
def remove(self, artifact_id: ArtifactId) -> Result[None, ErrorsList]:
|
|
128
|
+
if self.readonly:
|
|
129
|
+
return Err([world_errors.WorldReadonly(world_id=self.id)])
|
|
130
|
+
|
|
131
|
+
path = self._resolve_artifact_file(artifact_id).unwrap()
|
|
132
|
+
|
|
133
|
+
if path is None:
|
|
134
|
+
return Ok(None)
|
|
135
|
+
|
|
136
|
+
path.unlink()
|
|
137
|
+
|
|
138
|
+
return Ok(None)
|
|
139
|
+
|
|
126
140
|
@unwrap_to_error
|
|
127
141
|
def file_extension_for(self, artifact_id: ArtifactId) -> Result[str, ErrorsList]:
|
|
128
142
|
path = self._resolve_artifact_file(artifact_id).unwrap()
|