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.
Files changed (70) hide show
  1. donna/artifacts/intro.md +39 -0
  2. donna/artifacts/research/specs/report.md +163 -0
  3. donna/artifacts/research/work/research.md +198 -0
  4. donna/artifacts/rfc/specs/request_for_change.md +270 -0
  5. donna/artifacts/rfc/work/create.md +120 -0
  6. donna/artifacts/rfc/work/do.md +109 -0
  7. donna/artifacts/rfc/work/plan.md +68 -0
  8. donna/artifacts/usage/artifacts.md +55 -12
  9. donna/artifacts/usage/cli.md +114 -39
  10. donna/artifacts/usage/worlds.md +8 -2
  11. donna/cli/__main__.py +1 -1
  12. donna/cli/commands/artifacts.py +104 -17
  13. donna/cli/commands/sessions.py +8 -8
  14. donna/cli/commands/workspaces.py +42 -0
  15. donna/cli/errors.py +18 -0
  16. donna/cli/types.py +16 -9
  17. donna/cli/utils.py +2 -2
  18. donna/core/errors.py +1 -11
  19. donna/core/result.py +5 -8
  20. donna/core/utils.py +0 -3
  21. donna/lib/__init__.py +4 -0
  22. donna/lib/sources.py +1 -1
  23. donna/lib/worlds.py +2 -2
  24. donna/machine/action_requests.py +0 -5
  25. donna/machine/artifacts.py +8 -6
  26. donna/machine/primitives.py +5 -5
  27. donna/machine/sessions.py +13 -5
  28. donna/machine/state.py +4 -4
  29. donna/machine/tasks.py +4 -18
  30. donna/machine/templates.py +4 -2
  31. donna/primitives/artifacts/specification.py +13 -2
  32. donna/primitives/artifacts/workflow.py +11 -2
  33. donna/primitives/directives/list.py +86 -0
  34. donna/primitives/directives/view.py +52 -11
  35. donna/primitives/operations/finish_workflow.py +13 -2
  36. donna/primitives/operations/output.py +87 -0
  37. donna/primitives/operations/request_action.py +3 -9
  38. donna/primitives/operations/run_script.py +2 -2
  39. donna/protocol/utils.py +22 -0
  40. donna/workspaces/artifacts.py +238 -0
  41. donna/{world → workspaces}/artifacts_discovery.py +1 -1
  42. donna/{world → workspaces}/config.py +18 -11
  43. donna/{world → workspaces}/errors.py +55 -45
  44. donna/workspaces/initialization.py +78 -0
  45. donna/{world → workspaces}/markdown.py +21 -26
  46. donna/{world → workspaces}/sources/base.py +2 -2
  47. donna/{world → workspaces}/sources/markdown.py +8 -7
  48. donna/{world → workspaces}/templates.py +4 -4
  49. donna/workspaces/tmp.py +51 -0
  50. donna/{world → workspaces}/worlds/base.py +6 -3
  51. donna/{world → workspaces}/worlds/filesystem.py +30 -10
  52. donna/{world → workspaces}/worlds/python.py +12 -9
  53. donna-0.2.2.dist-info/METADATA +463 -0
  54. donna-0.2.2.dist-info/RECORD +92 -0
  55. {donna-0.2.0.dist-info → donna-0.2.2.dist-info}/WHEEL +1 -1
  56. donna/artifacts/work/do_it.md +0 -142
  57. donna/artifacts/work/do_it_fast.md +0 -98
  58. donna/artifacts/work/planning.md +0 -245
  59. donna/cli/commands/projects.py +0 -49
  60. donna/world/artifacts.py +0 -122
  61. donna/world/initialization.py +0 -42
  62. donna/world/tmp.py +0 -33
  63. donna/world/worlds/__init__.py +0 -0
  64. donna-0.2.0.dist-info/METADATA +0 -44
  65. donna-0.2.0.dist-info/RECORD +0 -85
  66. /donna/{artifacts/work → workspaces}/__init__.py +0 -0
  67. /donna/{world → workspaces}/sources/__init__.py +0 -0
  68. /donna/{world → workspaces/worlds}/__init__.py +0 -0
  69. {donna-0.2.0.dist-info → donna-0.2.2.dist-info}/entry_points.txt +0 -0
  70. {donna-0.2.0.dist-info → donna-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -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.world import errors as world_errors
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 = sections[-1]
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 section.title is not None:
119
- return Err([world_errors.MarkdownMultipleH1Titles(artifact_id=artifact_id)])
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
- section.title = clear_heading(render_back(node.to_tokens()).strip())
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 section.title is None:
132
- return Err([world_errors.MarkdownH2BeforeH1Title(artifact_id=artifact_id)])
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
- section.original_tokens.extend(node.to_tokens())
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.world.artifacts import ArtifactRenderContext
17
- from donna.world.config import SourceConfig as SourceConfigModel
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.world import errors as world_errors
10
- from donna.world import markdown
11
- from donna.world.artifacts import ArtifactRenderContext
12
- from donna.world.sources.base import SourceConfig, SourceConstructor
13
- from donna.world.templates import RenderMode, render
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
  )
@@ -140,7 +141,7 @@ def parse_artifact_content(
140
141
  )
141
142
 
142
143
  if not original_sections:
143
- # return Envrironment errors
144
+ # return Environment errors
144
145
  return Err([world_errors.MarkdownArtifactWithoutSections(artifact_id=full_id)])
145
146
 
146
147
  for original, analyzed in zip(original_sections, analyzed_sections):
@@ -257,4 +258,4 @@ def _ensure_markdown_constructible(
257
258
 
258
259
 
259
260
  if TYPE_CHECKING:
260
- from donna.world.config import SourceConfig as SourceConfigModel
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.world import errors as world_errors
15
+ from donna.workspaces import errors as world_errors
16
16
 
17
17
  if TYPE_CHECKING:
18
- from donna.world.artifacts import ArtifactRenderContext
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:
@@ -0,0 +1,51 @@
1
+ import pathlib
2
+ import shutil
3
+ import time
4
+
5
+ from donna.cli.types import FullArtifactIdArgument
6
+ from donna.workspaces.config import config, config_dir
7
+
8
+
9
+ def dir() -> pathlib.Path:
10
+ cfg = config()
11
+ tmp_path = cfg.tmp_dir
12
+
13
+ if not cfg.tmp_dir.is_absolute():
14
+ tmp_path = config_dir() / tmp_path
15
+
16
+ tmp_path.mkdir(parents=True, exist_ok=True)
17
+
18
+ return tmp_path
19
+
20
+
21
+ def file_for_artifact(artifact_id: FullArtifactIdArgument, extension: str) -> pathlib.Path:
22
+ directory = dir()
23
+
24
+ directory.mkdir(parents=True, exist_ok=True)
25
+
26
+ normalized_extension = extension.lstrip(".")
27
+ artifact_file_name = f"{str(artifact_id).replace('/', '.')}.{int(time.time() * 1000)}.{normalized_extension}"
28
+
29
+ return directory / artifact_file_name
30
+
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
+
50
+ def clear() -> None:
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.world.artifacts import ArtifactRenderContext
15
- from donna.world.config import WorldConfig
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
 
@@ -47,7 +50,7 @@ class World(BaseEntity, ABC):
47
50
  # For the artifact: uniform API for storing/loading data
48
51
  # Against the artifact:
49
52
  # - session data MUST be accessible only by Donna => no one should be able to read/write/list it
50
- # - session data will require an additonal kind(s) of artifact(s) just for that purpose
53
+ # - session data will require an additional kind(s) of artifact(s) just for that purpose
51
54
  # - session data may change more frequently than regular artifacts
52
55
 
53
56
  @abstractmethod
@@ -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.world import errors as world_errors
10
- from donna.world.artifacts import ArtifactRenderContext
11
- from donna.world.artifacts_discovery import ArtifactListingNode, list_artifacts_by_pattern
12
- from donna.world.worlds.base import World as BaseWorld
13
- from donna.world.worlds.base import WorldConstructor
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.world.config import SourceConfigValue, WorldConfig
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.world.config import config
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.world.config import config
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.world.config import config
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()
@@ -181,9 +195,15 @@ class FilesystemWorldConstructor(WorldConstructor):
181
195
  if path_value is None:
182
196
  raise ValueError(f"World config '{config.id}' does not define a filesystem path")
183
197
 
198
+ from donna.workspaces.config import project_dir
199
+
200
+ path = pathlib.Path(path_value).expanduser()
201
+ if not path.is_absolute():
202
+ path = project_dir() / path
203
+
184
204
  return World(
185
205
  id=config.id,
186
- path=pathlib.Path(path_value),
206
+ path=path.resolve(),
187
207
  readonly=config.readonly,
188
208
  session=config.session,
189
209
  )
@@ -7,14 +7,14 @@ from donna.core.errors import ErrorsList
7
7
  from donna.core.result import Err, Ok, Result, unwrap_to_error
8
8
  from donna.domain.ids import ArtifactId, FullArtifactId, FullArtifactIdPattern, WorldId
9
9
  from donna.machine.artifacts import Artifact
10
- from donna.world import errors as world_errors
11
- from donna.world.artifacts import ArtifactRenderContext
12
- from donna.world.artifacts_discovery import ArtifactListingNode, list_artifacts_by_pattern
13
- from donna.world.worlds.base import World as BaseWorld
14
- from donna.world.worlds.base import WorldConstructor
10
+ from donna.workspaces import errors as world_errors
11
+ from donna.workspaces.artifacts import ArtifactRenderContext
12
+ from donna.workspaces.artifacts_discovery import ArtifactListingNode, list_artifacts_by_pattern
13
+ from donna.workspaces.worlds.base import World as BaseWorld
14
+ from donna.workspaces.worlds.base import WorldConstructor
15
15
 
16
16
  if TYPE_CHECKING:
17
- from donna.world.config import SourceConfigValue, WorldConfig
17
+ from donna.workspaces.config import SourceConfigValue, WorldConfig
18
18
 
19
19
 
20
20
  class Python(BaseWorld):
@@ -59,7 +59,7 @@ class Python(BaseWorld):
59
59
  if not resource_dir.is_dir():
60
60
  return Ok(None)
61
61
 
62
- from donna.world.config import config
62
+ from donna.workspaces.config import config
63
63
 
64
64
  supported_extensions = config().supported_extensions()
65
65
  matches = [
@@ -81,7 +81,7 @@ class Python(BaseWorld):
81
81
  def _get_source_by_filename(
82
82
  self, artifact_id: ArtifactId, filename: str
83
83
  ) -> Result["SourceConfigValue", ErrorsList]:
84
- from donna.world.config import config
84
+ from donna.workspaces.config import config
85
85
 
86
86
  extension = pathlib.Path(filename).suffix
87
87
  source_config = config().find_source_for_extension(extension)
@@ -115,7 +115,7 @@ class Python(BaseWorld):
115
115
  full_id = FullArtifactId((self.id, artifact_id))
116
116
 
117
117
  extension = pathlib.Path(resource_path.name).suffix
118
- from donna.world.config import config
118
+ from donna.workspaces.config import config
119
119
 
120
120
  source_config = config().find_source_for_extension(extension)
121
121
  if source_config is None:
@@ -142,6 +142,9 @@ class Python(BaseWorld):
142
142
  def update(self, artifact_id: ArtifactId, content: bytes, extension: str) -> Result[None, ErrorsList]:
143
143
  return Err([world_errors.WorldReadonly(world_id=self.id)])
144
144
 
145
+ def remove(self, artifact_id: ArtifactId) -> Result[None, ErrorsList]:
146
+ return Err([world_errors.WorldReadonly(world_id=self.id)])
147
+
145
148
  @unwrap_to_error
146
149
  def file_extension_for(self, artifact_id: ArtifactId) -> Result[str, ErrorsList]:
147
150
  resource_path = self._resolve_artifact_file(artifact_id).unwrap()