memoryframes 0.4.0__tar.gz → 0.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: memoryframes
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Add your description here
5
5
  Author: David Brownell
6
6
  Author-email: David Brownell <github@DavidBrownell.com>
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.14
13
13
  Requires-Dist: dbrownell-common>=0.16.0
14
14
  Requires-Dist: pluggy>=1.6.0
15
15
  Requires-Dist: python-frontmatter>=1.1.0
16
+ Requires-Dist: rtyaml>=1.0.0
16
17
  Requires-Dist: textual>=6.8.0
17
18
  Requires-Dist: typer>=0.20.0
18
19
  Requires-Dist: typer-config>=1.4.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "MemoryFrames"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  # ^^^^^
5
5
  # Wheel names will be generated according to this value. Do not manually modify this value; instead
6
6
  # update it according to committed changes by running this command from the root of the repository:
@@ -17,6 +17,7 @@ dependencies = [
17
17
  "dbrownell-common>=0.16.0",
18
18
  "pluggy>=1.6.0",
19
19
  "python-frontmatter>=1.1.0",
20
+ "rtyaml>=1.0.0",
20
21
  "textual>=6.8.0",
21
22
  "typer>=0.20.0",
22
23
  "typer-config>=1.4.3",
@@ -32,6 +33,9 @@ classifiers = [
32
33
  [project.license]
33
34
  text = "MIT"
34
35
 
36
+ [project.scripts]
37
+ memoryframes = "MemoryFrames.__main__:app"
38
+
35
39
  [project.urls]
36
40
  Homepage = "https://github.com/davidbrownell/MemoryFrames"
37
41
  Documentation = "https://github.com/davidbrownell/MemoryFrames"
@@ -47,6 +51,7 @@ dev = [
47
51
  "dbrownell-commitemojis>=0.2.0",
48
52
  "pre-commit>=4.5.0",
49
53
  "py-minisign>=0.13.0",
54
+ "pyfakefs>=5.10.2",
50
55
  "pytest>=9.0.2",
51
56
  "pytest-cov>=7.0.0",
52
57
  "ruff>=0.14.9",
@@ -1,7 +1,6 @@
1
- from abc import ABC, abstractmethod
2
1
  from dataclasses import dataclass
3
2
  from functools import cached_property
4
- from typing import TYPE_CHECKING
3
+ from typing import ClassVar, TYPE_CHECKING
5
4
 
6
5
  import pluggy
7
6
 
@@ -31,14 +30,31 @@ class ThreadInfo:
31
30
 
32
31
 
33
32
  # ----------------------------------------------------------------------
34
- class Plugin(ABC):
33
+ class Plugin:
35
34
  """Base class for all MemoryFrames plugins."""
36
35
 
36
+ # ----------------------------------------------------------------------
37
+ NAME: ClassVar[str] = ""
38
+ """The name of the plugin."""
39
+
40
+ AUTHOR: ClassVar[str] = ""
41
+ """The author of the plugin. This value will be used in the plugin's unique name."""
42
+
43
+ DESCRIPTION: ClassVar[str] = ""
44
+ """The description of the plugin."""
45
+
46
+ PLUGIN_PRIORITY: ClassVar[int] = 0
47
+ """The priority of the plugin. Higher values indicate higher priority, which will cause the plugin to appear before other lower priority plugins."""
48
+
37
49
  # ----------------------------------------------------------------------
38
50
  def __init__(
39
51
  self,
40
52
  root_data_dir: Path,
41
53
  ) -> None:
54
+ assert self.__class__.NAME, "Derived classes must set the NAME class variable"
55
+ assert self.__class__.AUTHOR, "Derived classes must set the AUTHOR class variable"
56
+ assert self.__class__.DESCRIPTION, "Derived classes must set the DESCRIPTION class variable"
57
+
42
58
  scrubbed_unique_name = self.unique_name
43
59
 
44
60
  for invalid_char in ["<", ">", ":", '"', "/", "\\", "|", "?", "*", "."]:
@@ -47,34 +63,10 @@ class Plugin(ABC):
47
63
  self.working_dir = root_data_dir / scrubbed_unique_name
48
64
 
49
65
  # ----------------------------------------------------------------------
50
- @property
51
- @abstractmethod
52
- def name(self) -> str:
53
- """The name of the plugin."""
54
- raise NotImplementedError() # pragma: no cover
55
-
56
- @property
57
- @abstractmethod
58
- def author(self) -> str:
59
- """The author of the plugin. This value will be used in the plugin's unique name."""
60
- raise NotImplementedError() # pragma: no cover
61
-
62
- @property
63
- @abstractmethod
64
- def description(self) -> str:
65
- """The description of the plugin."""
66
- raise NotImplementedError() # pragma: no cover
67
-
68
- @property
69
- @abstractmethod
70
- def plugin_priority(self) -> int:
71
- """The priority of the plugin. Higher values indicate higher priority, which will cause the plugin to appear before other lower priority plugins."""
72
- raise NotImplementedError() # pragma: no cover
73
-
74
66
  @cached_property
75
67
  def unique_name(self) -> str:
76
68
  """The unique name of the plugin."""
77
- return f"{self.author}_{self.name}"
69
+ return f"{self.__class__.AUTHOR}_{self.__class__.NAME}"
78
70
 
79
71
  # ----------------------------------------------------------------------
80
72
  def GetNoteSource(
@@ -0,0 +1,86 @@
1
+ from typing import cast, TYPE_CHECKING
2
+
3
+ import rtyaml
4
+
5
+
6
+ if TYPE_CHECKING:
7
+ from pathlib import Path
8
+
9
+ from MemoryFrames.PluginInfra.Plugin import Plugin
10
+
11
+
12
+ # ----------------------------------------------------------------------
13
+ class Settings:
14
+ """Settings used by MemoryFrames and its plugins."""
15
+
16
+ # ----------------------------------------------------------------------
17
+ @classmethod
18
+ def DeserializeOrCreate(
19
+ cls,
20
+ content_dir: Path,
21
+ working_dir: Path,
22
+ *,
23
+ verbose: bool,
24
+ debug: bool,
25
+ ) -> Settings:
26
+ """Deserialize settings from disk or create default settings if necessary."""
27
+
28
+ settings_filename = cls._GetSettingsFilename(working_dir)
29
+
30
+ if settings_filename.is_file():
31
+ with settings_filename.open("r", encoding="utf-8") as f:
32
+ settings = rtyaml.load(f)
33
+ else:
34
+ settings = {}
35
+
36
+ return cls(
37
+ content_dir,
38
+ working_dir,
39
+ cast(dict[str, object], settings),
40
+ verbose=verbose,
41
+ debug=debug,
42
+ )
43
+
44
+ # ----------------------------------------------------------------------
45
+ def __init__(
46
+ self,
47
+ content_dir: Path,
48
+ working_dir: Path,
49
+ settings: dict[str, object],
50
+ *,
51
+ verbose: bool,
52
+ debug: bool,
53
+ ) -> None:
54
+ if debug is True:
55
+ verbose = True
56
+
57
+ # Commit results
58
+ self.content_dir = content_dir
59
+ self.working_dir = working_dir
60
+
61
+ self.verbose = verbose
62
+ self.debug = debug
63
+
64
+ self._settings = settings
65
+
66
+ # ----------------------------------------------------------------------
67
+ def Serialize(self) -> None:
68
+ """Serialize settings to disk."""
69
+
70
+ settings_filename = self._GetSettingsFilename(self.working_dir)
71
+
72
+ with settings_filename.open("w", encoding="utf-8") as f:
73
+ rtyaml.dump(self._settings, f)
74
+
75
+ # ----------------------------------------------------------------------
76
+ def GetPluginSettings(self, plugin: type[Plugin]) -> dict[str, object]:
77
+ """Return the settings associated with the provided Plugin."""
78
+
79
+ return cast(dict[str, object], self._settings.get(f"{plugin.AUTHOR}_{plugin.NAME}", {}))
80
+
81
+ # ----------------------------------------------------------------------
82
+ # ----------------------------------------------------------------------
83
+ # ----------------------------------------------------------------------
84
+ @staticmethod
85
+ def _GetSettingsFilename(working_dir: Path) -> Path:
86
+ return working_dir / "settings.yaml"
@@ -0,0 +1,11 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from MemoryFrames.Settings import Settings
5
+
6
+
7
+ # ----------------------------------------------------------------------
8
+ def Execute(settings: Settings) -> None:
9
+ """Execute the Textual user experience."""
10
+
11
+ print("TextualUserExperience", settings) # noqa: T201
@@ -0,0 +1,107 @@
1
+ import os
2
+
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from typer.core import TyperGroup
10
+
11
+ from MemoryFrames.Settings import Settings
12
+ from MemoryFrames import TextualUserExperience
13
+
14
+
15
+ # ----------------------------------------------------------------------
16
+ class NaturalOrderGrouper(TyperGroup): # noqa: D101
17
+ # ----------------------------------------------------------------------
18
+ def list_commands(self, *args, **kwargs) -> list[str]: # noqa: ARG002, D102
19
+ return list(self.commands.keys()) # pragma: no cover
20
+
21
+
22
+ # ----------------------------------------------------------------------
23
+ app = typer.Typer(
24
+ cls=NaturalOrderGrouper,
25
+ help=__doc__,
26
+ no_args_is_help=True,
27
+ pretty_exceptions_show_locals=False,
28
+ pretty_exceptions_enable=False,
29
+ )
30
+
31
+
32
+ # ----------------------------------------------------------------------
33
+ def _GetWorkingDir() -> Path:
34
+ data_dir = Path.home()
35
+
36
+ if os.name == "nt": # noqa: SIM108
37
+ data_dir = data_dir / "AppData" / "Local"
38
+ else:
39
+ data_dir = data_dir / ".local" / "share"
40
+
41
+ data_dir /= "MemoryFrames"
42
+
43
+ data_dir.mkdir(parents=True, exist_ok=True)
44
+
45
+ return data_dir
46
+
47
+
48
+ # ----------------------------------------------------------------------
49
+ class _ExperienceType(str, Enum):
50
+ Textual = "Textual"
51
+
52
+
53
+ # ----------------------------------------------------------------------
54
+ @app.command("EntryPoint", no_args_is_help=False)
55
+ def EntryPoint(
56
+ content_dir: Annotated[
57
+ Path,
58
+ typer.Option(
59
+ "--content-dir",
60
+ exists=True,
61
+ file_okay=False,
62
+ help="Directory containing notes to load.",
63
+ resolve_path=True,
64
+ ),
65
+ ] = Path.cwd(), # noqa: B008
66
+ working_dir: Annotated[
67
+ Path,
68
+ typer.Option(
69
+ "--working-dir",
70
+ file_okay=False,
71
+ help="Directory to use for settings and other persisted data.",
72
+ resolve_path=True,
73
+ ),
74
+ ] = _GetWorkingDir(), # noqa: B008
75
+ experience: Annotated[
76
+ _ExperienceType,
77
+ typer.Option("--experience", case_sensitive=False, help="The user experience to use."),
78
+ ] = _ExperienceType.Textual,
79
+ verbose: Annotated[ # noqa: FBT002
80
+ bool,
81
+ typer.Option("--verbose", help="Write verbose information to the terminal."),
82
+ ] = False,
83
+ debug: Annotated[ # noqa: FBT002
84
+ bool,
85
+ typer.Option("--debug", help="Write debug information to the terminal."),
86
+ ] = False,
87
+ ) -> None:
88
+ """Run MemoryFrames."""
89
+
90
+ content_dir.mkdir(parents=True, exist_ok=True)
91
+ working_dir.mkdir(parents=True, exist_ok=True)
92
+
93
+ settings = Settings.DeserializeOrCreate(content_dir, working_dir, verbose=verbose, debug=debug)
94
+
95
+ if experience == _ExperienceType.Textual:
96
+ user_experience_func = TextualUserExperience.Execute
97
+ else:
98
+ assert False, experience # noqa: B011, PT015 # pragma: no cover
99
+
100
+ user_experience_func(settings)
101
+
102
+
103
+ # ----------------------------------------------------------------------
104
+ # ----------------------------------------------------------------------
105
+ # ----------------------------------------------------------------------
106
+ if __name__ == "__main__":
107
+ app() # pragma: no cover
File without changes