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
donna/cli/utils.py ADDED
@@ -0,0 +1,53 @@
1
+ import functools
2
+ import sys
3
+ from collections.abc import Iterable
4
+ from typing import Callable, ParamSpec
5
+
6
+ import typer
7
+
8
+ from donna.core.errors import EnvironmentError
9
+ from donna.core.result import UnwrapError
10
+ from donna.protocol.cells import Cell
11
+ from donna.protocol.modes import get_cell_formatter
12
+ from donna.world.initialization import initialize_environment
13
+
14
+
15
+ def output_cells(cells: Iterable[Cell]) -> None:
16
+ formatter = get_cell_formatter()
17
+
18
+ output = formatter.format_cells(list(cells))
19
+
20
+ sys.stdout.buffer.write(output)
21
+
22
+
23
+ P = ParamSpec("P")
24
+
25
+
26
+ def cells_cli(func: Callable[P, Iterable[Cell]]) -> Callable[P, None]:
27
+
28
+ @functools.wraps(func)
29
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
30
+ try:
31
+ cells = func(*args, **kwargs)
32
+ except UnwrapError as e:
33
+ if isinstance(e.arguments["error"], EnvironmentError):
34
+ cells = [e.arguments["error"].node().info()]
35
+ elif isinstance(e.arguments["error"], Iterable):
36
+ cells = [error.node().info() for error in e.arguments["error"] if isinstance(error, EnvironmentError)]
37
+ else:
38
+ raise
39
+
40
+ output_cells(cells)
41
+
42
+ return wrapper
43
+
44
+
45
+ def try_initialize_donna() -> None:
46
+ result = initialize_environment()
47
+
48
+ if result.is_ok():
49
+ return
50
+
51
+ output_cells([error.node().info() for error in result.unwrap_err()])
52
+
53
+ raise typer.Exit(code=0)
donna/core/__init__.py ADDED
File without changes
donna/core/entities.py ADDED
@@ -0,0 +1,27 @@
1
+ from typing import Any, TypeVar
2
+
3
+ import pydantic
4
+
5
+ BASE_ENTITY = TypeVar("BASE_ENTITY", bound="BaseEntity")
6
+
7
+
8
+ class BaseEntity(pydantic.BaseModel):
9
+ model_config = pydantic.ConfigDict(
10
+ str_strip_whitespace=True,
11
+ validate_default=True,
12
+ extra="forbid",
13
+ frozen=True,
14
+ validate_assignment=True,
15
+ from_attributes=False,
16
+ )
17
+
18
+ def replace(self: BASE_ENTITY, **kwargs: Any) -> BASE_ENTITY:
19
+ return self.model_copy(update=kwargs, deep=True)
20
+
21
+ def to_json(self) -> str:
22
+ # TODO: make indent configurable
23
+ return self.model_dump_json(indent=2)
24
+
25
+ @classmethod
26
+ def from_json(cls: type[BASE_ENTITY], json_data: str) -> BASE_ENTITY:
27
+ return cls.model_validate_json(json_data)
donna/core/errors.py ADDED
@@ -0,0 +1,126 @@
1
+ from typing import Any
2
+
3
+ import pydantic
4
+
5
+ from donna.core.entities import BaseEntity
6
+ from donna.protocol.cells import Cell, MetaValue, to_meta_value
7
+ from donna.protocol.nodes import Node
8
+
9
+
10
+ class InternalError(Exception):
11
+ message = "An internal error occurred"
12
+
13
+ def __init__(self, **kwargs: Any) -> None:
14
+ self.arguments = kwargs
15
+
16
+ def error_message(self) -> str:
17
+ return self.message.format(**self.arguments)
18
+
19
+ def __str__(self) -> str:
20
+ return f"{self.__class__.__name__}: {self.error_message()}"
21
+
22
+
23
+ class EnvironmentError(BaseEntity):
24
+ cell_kind: str
25
+ cell_media_type: str = "text/markdown"
26
+
27
+ code: str
28
+ message: str
29
+ ways_to_fix: list[str] = pydantic.Field(default_factory=list)
30
+
31
+ def content_intro(self) -> str:
32
+ return "Error"
33
+
34
+ def node(self) -> "EnvironmentErrorNode":
35
+ return EnvironmentErrorNode(self)
36
+
37
+
38
+ class EnvironmentErrorsProxy(InternalError):
39
+ message = "This is a technical exception to pass an environment error up the call stack."
40
+
41
+ def __init__(self, errors: list[EnvironmentError]) -> None:
42
+ super().__init__(errors=errors)
43
+
44
+
45
+ class CoreEnvironmentError(EnvironmentError):
46
+ """Base class for environment errors in donna.core."""
47
+
48
+ cell_kind: str = "core_environment_error"
49
+
50
+
51
+ class ProjectDirNotFound(CoreEnvironmentError):
52
+ code: str = "donna.core.project_dir_not_found"
53
+ message: str = "Could not find a project directory containing `{error.donna_dir_name}`."
54
+ ways_to_fix: list[str] = [
55
+ "Run Donna from within a project directory that contains the donna directory.",
56
+ "Create the donna directory in the project root if it is missing.",
57
+ ]
58
+ donna_dir_name: str
59
+
60
+
61
+ class ProjectDirIsHome(CoreEnvironmentError):
62
+ code: str = "donna.core.project_dir_is_home"
63
+ message: str = "The discovered `{error.donna_dir_name}` directory is the home directory, not a project directory."
64
+ ways_to_fix: list[str] = [
65
+ "Run Donna from within a project directory that contains the donna directory.",
66
+ "Move the donna directory out of the home folder into the project root if appropriate.",
67
+ ]
68
+ donna_dir_name: str
69
+
70
+
71
+ class EnvironmentErrorNode(Node):
72
+ __slots__ = ("_error",)
73
+
74
+ def __init__(self, environment_error: EnvironmentError) -> None:
75
+ self._error = environment_error
76
+
77
+ def meta(self) -> dict[str, MetaValue]:
78
+ meta: dict[str, MetaValue] = {
79
+ "error_code": self._error.code,
80
+ }
81
+
82
+ for field_name, _field in self._error.model_fields.items():
83
+ if field_name in ("code", "message", "cell_kind", "cell_media_type", "ways_to_fix"):
84
+ continue
85
+
86
+ value = getattr(self._error, field_name)
87
+
88
+ if value is None:
89
+ continue
90
+
91
+ meta[field_name] = to_meta_value(value)
92
+
93
+ return meta
94
+
95
+ def content(self) -> str:
96
+ intro = self._error.content_intro()
97
+
98
+ message = self._error.message.format(error=self._error).strip()
99
+
100
+ ways_to_fix = [fix.format(error=self._error).strip() for fix in self._error.ways_to_fix]
101
+
102
+ if "\n" in self._error.message:
103
+ content = f"{intro}:\n\n{message}"
104
+ else:
105
+ content = f"{intro}: {message}"
106
+
107
+ if not ways_to_fix:
108
+ return content
109
+
110
+ if len(ways_to_fix) == 1:
111
+ return f"{content}\nWay to fix: {ways_to_fix[0]}"
112
+
113
+ fixes = "\n".join(f"- {fix}" for fix in ways_to_fix)
114
+
115
+ return f"{content}\n\nWays to fix:\n\n{fixes}"
116
+
117
+ def status(self) -> Cell:
118
+ return Cell.build(
119
+ kind=self._error.cell_kind,
120
+ media_type=self._error.cell_media_type,
121
+ content=self.content(),
122
+ **self.meta(),
123
+ )
124
+
125
+
126
+ ErrorsList = list[EnvironmentError]
donna/core/result.py ADDED
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ from dataclasses import dataclass
5
+ from typing import Callable, Generic, ParamSpec, TypeVar, cast
6
+
7
+ from donna.core.errors import InternalError
8
+
9
+ T = TypeVar("T")
10
+ U = TypeVar("U")
11
+ E = TypeVar("E")
12
+ F = TypeVar("F")
13
+ P = ParamSpec("P")
14
+
15
+
16
+ class ResultError(InternalError):
17
+ """Base class for internal errors in donna.core.result."""
18
+
19
+
20
+ class UnwrapError(ResultError):
21
+ message: str = "Called unwrap on an Err value."
22
+
23
+
24
+ class UnwrapErrError(ResultError):
25
+ message: str = "Called unwrap_err on an Ok value."
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class Result(Generic[T, E]):
30
+ _is_ok: bool
31
+ _value: T | E
32
+
33
+ def is_ok(self) -> bool:
34
+ return self._is_ok
35
+
36
+ def is_err(self) -> bool:
37
+ return not self._is_ok
38
+
39
+ def ok(self) -> T | None:
40
+ if self._is_ok:
41
+ return cast(T, self._value)
42
+ return None
43
+
44
+ def err(self) -> E | None:
45
+ if not self._is_ok:
46
+ return cast(E, self._value)
47
+ return None
48
+
49
+ def unwrap(self) -> T:
50
+ if self._is_ok:
51
+ return cast(T, self._value)
52
+
53
+ raise UnwrapError(error=self.unwrap_err())
54
+
55
+ def unwrap_err(self) -> E:
56
+ if not self._is_ok:
57
+ return cast(E, self._value)
58
+
59
+ raise UnwrapErrError(value=self.unwrap())
60
+
61
+ def unwrap_or(self, default: U) -> T | U:
62
+ if self._is_ok:
63
+ return cast(T, self._value)
64
+ return default
65
+
66
+ def map(self, func: Callable[[T], U]) -> "Result[U, E]":
67
+ if self._is_ok:
68
+ return Result(True, func(cast(T, self._value)))
69
+ return Result(False, cast(E, self._value))
70
+
71
+ def map_err(self, func: Callable[[E], F]) -> "Result[T, F]":
72
+ if not self._is_ok:
73
+ return Result(False, func(cast(E, self._value)))
74
+ return Result(True, cast(T, self._value))
75
+
76
+
77
+ def Ok(value: T) -> Result[T, E]:
78
+ return Result(True, value)
79
+
80
+
81
+ def Err(error: E) -> Result[T, E]:
82
+ return Result(False, error)
83
+
84
+
85
+ def ok(result: Result[T, E]) -> bool:
86
+ return result.is_ok()
87
+
88
+
89
+ def unwrap_to_error(func: Callable[P, Result[T, E]]) -> Callable[P, Result[T, E]]:
90
+
91
+ @functools.wraps(func)
92
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[T, E]:
93
+
94
+ try:
95
+ return func(*args, **kwargs)
96
+ except UnwrapError as e:
97
+ return Err(cast(E, e.arguments["error"]))
98
+
99
+ return wrapper
donna/core/utils.py ADDED
@@ -0,0 +1,37 @@
1
+ import pathlib
2
+
3
+ from donna.core import errors as core_errors
4
+ from donna.core.result import Err, Ok, Result
5
+
6
+
7
+ def first_donna_dir(donna_dir_name: str) -> pathlib.Path | None:
8
+ """Get the first parent directory containing the donna directory.
9
+
10
+ Search from the current working directory upwards for a folder with donna directory (.donna by default).
11
+ """
12
+ current_dir = pathlib.Path.cwd().resolve()
13
+
14
+ for parent in [current_dir] + list(current_dir.parents):
15
+ donna_path = parent / donna_dir_name
16
+ if donna_path.is_dir():
17
+ return parent
18
+
19
+ return None
20
+
21
+
22
+ def donna_home_dir(donna_dir_name: str) -> pathlib.Path:
23
+ """Get the donna home directory in the user's home folder."""
24
+ return pathlib.Path.home() / donna_dir_name
25
+
26
+
27
+ def discover_project_dir(donna_dir_name: str) -> Result[pathlib.Path, core_errors.ErrorsList]:
28
+ """Discover the project directory by looking for the donna directory in parent folders."""
29
+ donna_dir = first_donna_dir(donna_dir_name)
30
+
31
+ if donna_dir is None:
32
+ return Err([core_errors.ProjectDirNotFound(donna_dir_name=donna_dir_name)])
33
+
34
+ if donna_dir == donna_home_dir(donna_dir_name):
35
+ return Err([core_errors.ProjectDirIsHome(donna_dir_name=donna_dir_name)])
36
+
37
+ return Ok(donna_dir)
File without changes
donna/domain/errors.py ADDED
@@ -0,0 +1,47 @@
1
+ from donna.core import errors as core_errors
2
+
3
+
4
+ class InternalError(core_errors.InternalError):
5
+ """Base class for internal errors in donna.domain."""
6
+
7
+
8
+ class EnvironmentError(core_errors.EnvironmentError):
9
+ """Base class for environment errors in donna.domain."""
10
+
11
+ cell_kind: str = "domain_error"
12
+
13
+
14
+ class InvalidInternalId(InternalError):
15
+ message: str = "Invalid InternalId: '{value}'."
16
+ value: str
17
+
18
+
19
+ class InvalidIdentifier(InternalError):
20
+ message: str = "Invalid identifier: '{value}'."
21
+ value: str
22
+
23
+
24
+ class InvalidIdPath(InternalError):
25
+ message: str = "Invalid {id_type}: '{value}'."
26
+ id_type: str
27
+ value: str
28
+
29
+
30
+ class InvalidIdFormat(EnvironmentError):
31
+ code: str = "donna.domain.invalid_id_format"
32
+ message: str = "Invalid {error.id_type}: '{error.value}'."
33
+ ways_to_fix: list[str] = [
34
+ "Ensure the value uses the expected format for {error.id_type}.",
35
+ ]
36
+ id_type: str
37
+ value: str
38
+
39
+
40
+ class InvalidIdPattern(EnvironmentError):
41
+ code: str = "donna.domain.invalid_id_pattern"
42
+ message: str = "Invalid {error.id_type}: '{error.value}'."
43
+ ways_to_fix: list[str] = [
44
+ "Use identifiers or '*'/'**' tokens separated by the expected delimiter.",
45
+ ]
46
+ id_type: str
47
+ value: str