nervous-notes 0.0.1__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.
Files changed (33) hide show
  1. nervous_notes-0.0.1/.gitignore +5 -0
  2. nervous_notes-0.0.1/LICENSE +19 -0
  3. nervous_notes-0.0.1/PKG-INFO +110 -0
  4. nervous_notes-0.0.1/README.md +89 -0
  5. nervous_notes-0.0.1/pyproject.toml +37 -0
  6. nervous_notes-0.0.1/src/nervous_notes/__init__.py +10 -0
  7. nervous_notes-0.0.1/src/nervous_notes/__main__.py +44 -0
  8. nervous_notes-0.0.1/src/nervous_notes/dsl.py +63 -0
  9. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Example - Daily Responsibility.py +75 -0
  10. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Example - Extrovert.py +34 -0
  11. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Example - Introvert.py +54 -0
  12. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Example - Recurring Note Generator.py +61 -0
  13. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Literate Programming - Hello World.py +53 -0
  14. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Mqtt Publisher.py +30 -0
  15. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Mqtt Subscriber.py +8 -0
  16. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Next Notes Manager.py +25 -0
  17. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Next-Notes MOC.py +91 -0
  18. nervous_notes-0.0.1/src/nervous_notes/example_scripts/Timer Demo.py +31 -0
  19. nervous_notes-0.0.1/src/nervous_notes/nervous_note.py +144 -0
  20. nervous_notes-0.0.1/src/nervous_notes/nervous_process.py +60 -0
  21. nervous_notes-0.0.1/src/nervous_notes/note_companion_script.py +190 -0
  22. nervous_notes-0.0.1/src/nervous_notes/note_embedded_script.py +112 -0
  23. nervous_notes-0.0.1/src/nervous_notes/orchestration.py +60 -0
  24. nervous_notes-0.0.1/src/nervous_notes/requirements.txt +5 -0
  25. nervous_notes-0.0.1/src/nervous_notes/util.py +105 -0
  26. nervous_notes-0.0.1/src/nervous_notes/vault_router.py +33 -0
  27. nervous_notes-0.0.1/src/nervous_notes/wrappers/external_messages.py +127 -0
  28. nervous_notes-0.0.1/src/nervous_notes/wrappers/file_watcher.py +145 -0
  29. nervous_notes-0.0.1/src/nervous_notes/wrappers/note_api.py +288 -0
  30. nervous_notes-0.0.1/src/nervous_notes/wrappers/scripting.py +156 -0
  31. nervous_notes-0.0.1/tests/experimental_test.py +211 -0
  32. nervous_notes-0.0.1/tests/nextnotes_moc_test.py +120 -0
  33. nervous_notes-0.0.1/tests/utils.py +96 -0
@@ -0,0 +1,5 @@
1
+ dist
2
+ .idea
3
+ .DS_Store
4
+ venv3.10
5
+
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.5
2
+ Name: nervous-notes
3
+ Version: 0.0.1
4
+ Summary: Personal knowledge management meets the actor model
5
+ Project-URL: Homepage, https://github.com/micseydel/nervous_notes
6
+ Project-URL: Issues, https://github.com/micseydel/nervous_notes/issues
7
+ Author-email: Michael Seydel <contact@micseydel.me>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: paho-mqtt==2.1.0
14
+ Requires-Dist: pykka==4.4.2
15
+ Requires-Dist: requests==2.34.2
16
+ Requires-Dist: ruamel-yaml==0.19.1
17
+ Requires-Dist: watchdog==6.0.0
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest>=7; extra == 'test'
20
+ Description-Content-Type: text/markdown
21
+
22
+ > an ACTOR is an active agent which plays a role on cue according to a script
23
+
24
+ ([A Universal Modular ACTOR Formalism for Artificial Intelligence, 1973](https://www.ijcai.org/Proceedings/73/Papers/027B.pdf))
25
+
26
+ Nervous Notes is a Python module for managing _actors_ that have an associated plaintext Markdown note for their memory.
27
+ Each Markdown note can have YAML at the start, gated with `---` on the lines before and after the YAML.
28
+
29
+ **There is no AI in this project**, the notes are "nervous" because the actor model allows them to _reflexively_ react to
30
+ events, like their note being modified by a human, or a message sent by another note. If someone wants the "reflex" to
31
+ be an AI invocation, they are free to do so.
32
+
33
+ Useful for
34
+ - hot-reloadable daemons with plaintext, human-friendly memory
35
+ - [literate programming](https://en.wikipedia.org/wiki/Literate_programming) (Knuth, 1984) and "knowledge based programming" (same 1973 paper)
36
+ - personal knowledge management (e.g. automating some of an Obsidian vault)
37
+
38
+ ## Quick Start
39
+
40
+ ```commandline
41
+ pip install nervous-notes
42
+ ```
43
+
44
+ `python -m` to `example_scripts` FIXME
45
+
46
+ example note FIXME
47
+ ```
48
+ ---
49
+ this_is: yaml
50
+ ---
51
+ - (markdown)
52
+ ```
53
+
54
+ ## Glossary
55
+
56
+ - FIXME
57
+ - companion
58
+ - embedded - for literate programming
59
+ - actor / agent
60
+ - note / nervous note
61
+
62
+ ## Standard event types
63
+
64
+ - FIXME (placeholder)
65
+ - note updated
66
+ - timer up
67
+ - mqtt message
68
+ - "other" pykka message
69
+
70
+ ## Note API
71
+
72
+ - FIXME (placeholder)
73
+
74
+ ## Polyglot programming
75
+
76
+ This module currently includes mqtt support, which allows for sending messages between programs in different languages.
77
+
78
+ ## Knowledge Based Programming
79
+
80
+ Again, from the actor model paper:
81
+
82
+ > KNOWLEDGE BASED PROGRAMMING is programming in an environment which has a substantial knowledge base in the application area for which the programs are intended.
83
+
84
+ ## Limitations
85
+
86
+ - Pykka's thread/GIL limits are inherited
87
+ - multiprocessing support is limited and experimental
88
+ - globals are shared between hot-reloaded **embedded** nervous notes
89
+ - if someone removes a function from a hot reloading script, they should replace it with `pass` in the body before removing it, or else the prior version will remain active
90
+ - there are currently no built-in circuit breakers
91
+
92
+ ### Interfacing with Obsidian
93
+
94
+ I found that updating a note _immediately_ after a note-modified event can cause Obsidian to ignore the update, so there
95
+ are some sleep() calls to accommodate the behavior.
96
+
97
+ ## History
98
+
99
+ I originally implemented this in Scala, where I called it "[woke notes](https://github.com/micseydel/tinker-casting/tree/main/scripts/woke_notes_package)" before spinning off this fully separate repo.
100
+
101
+ ## Future work
102
+
103
+ - remove mqtt as a default dependency, and make it, kafka, and rabbitmq [optional dependencies](https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#optional-dependencies)
104
+ - maybe a "whisper" optional dependency for AI transcription (since this was one of my primary use-cases)
105
+ - similar modules for other languages/platforms (e.g. JVM, Javascript, Go, etc.) to improve polyglot support
106
+ - is Obsidian Canvas rich enough to wire actors together into flows?
107
+ - improved isolation between companion notes
108
+ - replace blocking I/O with non-blocking
109
+ - replace Pykka with something more Akka-like
110
+ - this would likely be an _enormous_ undertaking
@@ -0,0 +1,89 @@
1
+ > an ACTOR is an active agent which plays a role on cue according to a script
2
+
3
+ ([A Universal Modular ACTOR Formalism for Artificial Intelligence, 1973](https://www.ijcai.org/Proceedings/73/Papers/027B.pdf))
4
+
5
+ Nervous Notes is a Python module for managing _actors_ that have an associated plaintext Markdown note for their memory.
6
+ Each Markdown note can have YAML at the start, gated with `---` on the lines before and after the YAML.
7
+
8
+ **There is no AI in this project**, the notes are "nervous" because the actor model allows them to _reflexively_ react to
9
+ events, like their note being modified by a human, or a message sent by another note. If someone wants the "reflex" to
10
+ be an AI invocation, they are free to do so.
11
+
12
+ Useful for
13
+ - hot-reloadable daemons with plaintext, human-friendly memory
14
+ - [literate programming](https://en.wikipedia.org/wiki/Literate_programming) (Knuth, 1984) and "knowledge based programming" (same 1973 paper)
15
+ - personal knowledge management (e.g. automating some of an Obsidian vault)
16
+
17
+ ## Quick Start
18
+
19
+ ```commandline
20
+ pip install nervous-notes
21
+ ```
22
+
23
+ `python -m` to `example_scripts` FIXME
24
+
25
+ example note FIXME
26
+ ```
27
+ ---
28
+ this_is: yaml
29
+ ---
30
+ - (markdown)
31
+ ```
32
+
33
+ ## Glossary
34
+
35
+ - FIXME
36
+ - companion
37
+ - embedded - for literate programming
38
+ - actor / agent
39
+ - note / nervous note
40
+
41
+ ## Standard event types
42
+
43
+ - FIXME (placeholder)
44
+ - note updated
45
+ - timer up
46
+ - mqtt message
47
+ - "other" pykka message
48
+
49
+ ## Note API
50
+
51
+ - FIXME (placeholder)
52
+
53
+ ## Polyglot programming
54
+
55
+ This module currently includes mqtt support, which allows for sending messages between programs in different languages.
56
+
57
+ ## Knowledge Based Programming
58
+
59
+ Again, from the actor model paper:
60
+
61
+ > KNOWLEDGE BASED PROGRAMMING is programming in an environment which has a substantial knowledge base in the application area for which the programs are intended.
62
+
63
+ ## Limitations
64
+
65
+ - Pykka's thread/GIL limits are inherited
66
+ - multiprocessing support is limited and experimental
67
+ - globals are shared between hot-reloaded **embedded** nervous notes
68
+ - if someone removes a function from a hot reloading script, they should replace it with `pass` in the body before removing it, or else the prior version will remain active
69
+ - there are currently no built-in circuit breakers
70
+
71
+ ### Interfacing with Obsidian
72
+
73
+ I found that updating a note _immediately_ after a note-modified event can cause Obsidian to ignore the update, so there
74
+ are some sleep() calls to accommodate the behavior.
75
+
76
+ ## History
77
+
78
+ I originally implemented this in Scala, where I called it "[woke notes](https://github.com/micseydel/tinker-casting/tree/main/scripts/woke_notes_package)" before spinning off this fully separate repo.
79
+
80
+ ## Future work
81
+
82
+ - remove mqtt as a default dependency, and make it, kafka, and rabbitmq [optional dependencies](https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#optional-dependencies)
83
+ - maybe a "whisper" optional dependency for AI transcription (since this was one of my primary use-cases)
84
+ - similar modules for other languages/platforms (e.g. JVM, Javascript, Go, etc.) to improve polyglot support
85
+ - is Obsidian Canvas rich enough to wire actors together into flows?
86
+ - improved isolation between companion notes
87
+ - replace blocking I/O with non-blocking
88
+ - replace Pykka with something more Akka-like
89
+ - this would likely be an _enormous_ undertaking
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling >= 1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nervous-notes"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="Michael Seydel", email="contact@micseydel.me" },
10
+ ]
11
+ description = "Personal knowledge management meets the actor model"
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ license = "MIT"
19
+ license-files = ["LICEN[CS]E*"]
20
+ dependencies = [
21
+ "pykka==4.4.2",
22
+ "ruamel.yaml==0.19.1",
23
+ "paho-mqtt==2.1.0",
24
+ "watchdog==6.0.0",
25
+ "requests==2.34.2",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ test = ["pytest>=7"]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
33
+ pythonpath = ["src"]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/micseydel/nervous_notes"
37
+ Issues = "https://github.com/micseydel/nervous_notes/issues"
@@ -0,0 +1,10 @@
1
+ from .wrappers.external_messages import get_config_from_env
2
+ from .nervous_note import NervousNote
3
+ from .note_embedded_script import NoteEmbeddedScript
4
+ from .nervous_process import NervousProcess
5
+ from multiprocessing.connection import Connection
6
+ from . import dsl
7
+ __all__ = [
8
+ "NervousNote", "NoteEmbeddedScript", "NervousProcess", "get_config_from_env", "dsl",
9
+ "Connection", # for typing
10
+ ]
@@ -0,0 +1,44 @@
1
+ import logging
2
+ import argparse
3
+
4
+ from .orchestration import Orchestrator
5
+ from .nervous_note import NervousNote
6
+ from .wrappers.external_messages import get_config_from_env
7
+
8
+
9
+ logging.basicConfig(level=logging.INFO,
10
+ format='[%(asctime)s %(pathname)s:%(lineno)s] - %(message)s',
11
+ datefmt='%Y-%m-%d %H:%M:%S')
12
+
13
+ parser = argparse.ArgumentParser()
14
+ parser.add_argument("vault_path", help="Path to the vault directory", type=str)
15
+
16
+ # FIXME: need to make sure this is properly optional, in the Orchestrator
17
+ parser.add_argument("--scripts", help="The directory for scripts")
18
+
19
+ args = parser.parse_args()
20
+
21
+ mqtt_config = get_config_from_env()
22
+ if mqtt_config:
23
+ logging.info(f"Using vault {args.vault_path}, mqtt broker {mqtt_config.broker}:{mqtt_config.port} for user {mqtt_config.username}, and scripts_dir {args.scripts}")
24
+ else:
25
+ logging.info(f"Failed to get mqtt config, proceeding with None config...")
26
+
27
+ NervousNote.start_background_actors(args.vault_path, mqtt_config)
28
+ # FIXME: can I delete the password to let it get garbage collected?
29
+
30
+ deploy_debug_notes = False
31
+ # pykka.DeadLetterRouter.default_router().start() #?
32
+ orchestrator = Orchestrator.wake("Nervous Notes Orchestrator", args.scripts, deploy_debug_notes)
33
+
34
+ try:
35
+ while True:
36
+ input()
37
+ except KeyboardInterrupt:
38
+ logging.info("KeyboardInterrupt detected, stopping...")
39
+ orchestrator.stop()
40
+ logging.info("Orchestrator stopped")
41
+ NervousNote.stop_background_actors()
42
+
43
+ # FIXME: what more is needed for a proper, clean shutdown?
44
+ # exit(0)
@@ -0,0 +1,63 @@
1
+ """
2
+ used for typing, not at runtime
3
+
4
+ runtime comes from scriping.CompiledScript.script_scope
5
+ """
6
+
7
+ import logging
8
+ import json
9
+ import datetime
10
+ from time import ctime, sleep
11
+
12
+ import requests
13
+ from pykka import ActorRef
14
+
15
+ from .note_companion_script import TemplateScript
16
+ from .util import TimeUtil, Clock
17
+
18
+ from .nervous_note import MqttWrapper
19
+ from .wrappers.note_api import datetimestamped_markdown_list_line, timestamped_markdown_list_line, NoteAPI
20
+
21
+
22
+ my_note: NoteAPI
23
+ note_name: str
24
+ topic: str
25
+ default_topic: str
26
+ mqtt: MqttWrapper
27
+
28
+ _clock = Clock()
29
+ _timeutils = TimeUtil(_clock)
30
+ next_occurrence = _timeutils.next_occurrence
31
+ seconds_until = _timeutils.seconds_until
32
+ today = _clock.today
33
+
34
+
35
+ # may be expanded beyond template scripts
36
+ def wake(notename: str, template_script_name: TemplateScript) -> ActorRef: pass
37
+
38
+
39
+ def set_timer(delay_seconds: float | int, payload: bytes | None = None, key: str | None = None) -> None: pass
40
+
41
+
42
+ def cancel_timer(key: str) -> None: pass
43
+
44
+
45
+ def subscribe_internal(topic: str, subscriber) -> None: pass
46
+
47
+
48
+ def publish_internal(topic: str, payload: bytes) -> None: pass
49
+
50
+
51
+ def on_mqtt_message(topic: str, payload: bytes) -> None: pass
52
+
53
+
54
+ def on_note_modified() -> None: pass
55
+
56
+
57
+ def on_start() -> None: pass
58
+
59
+
60
+ def on_timer(key: str, payload: object) -> None: pass
61
+
62
+
63
+ def publish_to_ntfy(channel: str, message: str) -> None: pass
@@ -0,0 +1,75 @@
1
+ if TYPE_CHECKING: # IDE support
2
+ from ..dsl import *
3
+
4
+
5
+ def on_start():
6
+ reset_timer_from_note() # THERE's ALWAYS _a_ TIMER, for later today or tomorrow
7
+
8
+
9
+ def on_note_modified():
10
+ note = my_note.note_if_markdown_starts_with_pressed_button()
11
+ if note:
12
+ _frontmatter, markdown = note
13
+ sleep(0.25)
14
+ if today().isoformat() not in markdown:
15
+ updated_markdown = markdown_list_prepender(markdown)
16
+ reset_timer_from_note(note)
17
+ my_note.set_markdown(updated_markdown)
18
+ else:
19
+ my_note.reset_button_at_start_of_markdown()
20
+
21
+
22
+ def on_timer(_key, _payload):
23
+ note = frontmatter, _markdown = my_note.get_note()
24
+ channel = frontmatter.get("channel")
25
+ message = frontmatter.get("message")
26
+ logging.info(f"Timer called, publishing to {channel}: {message}")
27
+ publish_to_ntfy(channel, message)
28
+ reset_timer_from_note(note)
29
+
30
+
31
+ # utils
32
+
33
+ def markdown_list_prepender(markdown: str) -> str:
34
+ """
35
+ - reset the button
36
+ - prepend today's date (after the button)
37
+ """
38
+ lines = markdown.splitlines()
39
+ lines[0] = "- [ ] Mark as done"
40
+ line_to_prepend = f"- [[{my_note.note_name} ({today().isoformat()})]]"
41
+ lines.insert(1, line_to_prepend)
42
+ lines[-1] = lines[-1] + "\n"
43
+ return "\n".join(lines)
44
+
45
+
46
+ def reset_timer_from_note(note=None):
47
+ """
48
+ resets the timer for later today or tomorrow
49
+ """
50
+ if note is None:
51
+ note = my_note.get_note()
52
+
53
+ if note is not None:
54
+ frontmatter, markdown = note
55
+
56
+ next_notification = next_occurrence(frontmatter.get("ifNotDoneBy"))
57
+ rightnows_today = today()
58
+
59
+ today_marked_as_done = rightnows_today.isoformat() in markdown
60
+ next_notification_is_for_today = ((rightnows_today.year, rightnows_today.month, rightnows_today.day) ==
61
+ (next_notification.year, next_notification.month, next_notification.day))
62
+
63
+ next_notification_already_done = today_marked_as_done and next_notification_is_for_today
64
+ if next_notification_already_done:
65
+ next_notification += datetime.timedelta(days=1)
66
+
67
+ delay = seconds_until(next_notification)
68
+ logging.info(f"Setting timer for {next_notification}, delay {delay}s")
69
+ set_timer(
70
+ delay,
71
+ None, # no payload for this timer
72
+ key=f"{my_note.note_name}/TIMER" # there's just one timer
73
+ )
74
+ else:
75
+ logging.warning("No note; need yaml for {channel, message, ifNotDoneBy} (e.g. ifNotDoneBy = 20:32:00-07:00)")
@@ -0,0 +1,34 @@
1
+ def on_start():
2
+ my_note.set_file_contents(f"""---
3
+ in_topic: {default_topic}
4
+ out_topic: {default_topic}/publish
5
+ message_to_send: "Hello :)"
6
+ ---
7
+ - [ ] Send message
8
+ - Instructions:
9
+ - `out_topic` is updated live, but `in_topic` is currently provided but not read back
10
+ - ---
11
+ """)
12
+
13
+ def on_mqtt_message(topic, message):
14
+ my_note.append_datetimestamped_markdown_list_line(f"Received on {topic} message: {message.decode()}")
15
+
16
+ def on_note_modified():
17
+ if (maybe_note := my_note.note_if_markdown_starts_with_pressed_button()) is not None:
18
+ frontmatter, markdown = maybe_note
19
+ else:
20
+ return # not a note update we care about
21
+
22
+ if (out_topic := frontmatter.get("out_topic")) is None:
23
+ logging.warning("Button pressed but Frontmatter out_topic was empty")
24
+ return
25
+
26
+ if (message_to_send := frontmatter.get("message_to_send")) is None:
27
+ logging.warning("Button pressed but Frontmatter message_to_send was empty")
28
+ return
29
+
30
+ # introvert expects `(sender)> (message)` where sender is who they will reply to
31
+ mqtt.publish(out_topic, f"{topic}> {message_to_send}")
32
+
33
+ sleep(.25) # hack so Obsidian sees the update (otherwise it seems to miss it sometimes)
34
+ my_note.reset_button_at_start_of_markdown()
@@ -0,0 +1,54 @@
1
+ def on_start():
2
+ my_note.set_file_contents(f"""---
3
+ in_topic: {default_topic}
4
+ reply_with: "👋"
5
+ ---
6
+ - [ ] Pause replying
7
+ - Instructions:
8
+ - Change `reply_with` to whatever you would like (`in_topic` does not read back updates currently)
9
+ - delay_seconds is an optional field, assumed to be 0 (seconds)
10
+ - ---
11
+ """)
12
+
13
+ sending = True
14
+
15
+ def on_mqtt_message(topic, message):
16
+ config = my_note.get_frontmatter()
17
+
18
+ try:
19
+ # expects:
20
+ # sending_topic> message
21
+ sender, what_they_sent = message.decode().split(">", 1)
22
+ except ValueError:
23
+ my_note.append_datetimestamped_markdown_list_line(f"Received a weirdly formatted message: {message}")
24
+ return
25
+
26
+ if not sending:
27
+ my_note.append_datetimestamped_markdown_list_line(f"{sender} sent {what_they_sent}, not replying (sending paused)")
28
+ return
29
+
30
+ delay_seconds = config.get("delay_seconds", 0)
31
+
32
+ if (reply_with := config.get("reply_with")) is not None:
33
+ if delay_seconds:
34
+ set_timer(delay_seconds, (sender, reply_with))
35
+ my_note.append_datetimestamped_markdown_list_line(f"Received {what_they_sent}, replying with {reply_with} after {delay_seconds}s")
36
+ else:
37
+ my_note.append_datetimestamped_markdown_list_line(f"Received {what_they_sent}, replying with {reply_with}")
38
+ mqtt.publish(sender, reply_with)
39
+ else:
40
+ my_note.append_datetimestamped_markdown_list_line(f"{sender} sent {what_they_sent}, not replying (no reply_with frontmatter)")
41
+
42
+ def on_timer(key, payload):
43
+ sender, reply_with = payload
44
+ mqtt.publish(sender, reply_with)
45
+
46
+
47
+ def on_note_modified():
48
+ global sending # nonlocal doesn't work because sending is not enclosed in a function
49
+
50
+ maybe_note = my_note.note_if_markdown_starts_with_pressed_button()
51
+ if maybe_note is not None:
52
+ sending = False
53
+ else:
54
+ sending = True
@@ -0,0 +1,61 @@
1
+ from datetime import timedelta
2
+ from typing import io
3
+
4
+ if TYPE_CHECKING: # IDE support
5
+ from dsl import *
6
+
7
+
8
+ class Frontmatter:
9
+ pass
10
+
11
+
12
+ class Document:
13
+ frontmatter: Frontmatter
14
+ markdown: str
15
+
16
+
17
+ def on_start():
18
+ note = my_note.get_note()
19
+ if note is not None:
20
+ frontmatter, markdown = note
21
+
22
+ next_date = frontmatter.get("next_date")
23
+ default_interval_days = frontmatter.get("default_interval_days")
24
+
25
+ _button, current_next_note, *history = markdown.splitlines()
26
+
27
+ if current_next_note[:8] != "- Next: " or not current_next_note.endswith(")]]"):
28
+ logging.warning(f"Expected second line to start with `- Next: ` then a wikilink of the format [[Note (YYYY-MM-DD)]]")
29
+ return
30
+
31
+ current_next_date = current_next_note[-13:-3]
32
+ if next_date is None:
33
+ next_date = datetime.datetime.strptime(current_next_date, "%Y-%m-%d") + timedelta(days=default_interval_days)
34
+
35
+
36
+ def on_note_modified():
37
+ note = my_note.note_if_markdown_starts_with_pressed_button()
38
+ if note:
39
+ frontmatter, markdown = note
40
+ # FIXME
41
+ stream = io.StringIO()
42
+ my_note.yaml.dump(data, stream)
43
+
44
+ # Extract the YAML string
45
+ yaml_string = stream.getvalue()
46
+
47
+
48
+ # utils
49
+
50
+ # FIXME: factor out?
51
+ def markdown_list_prepender(markdown: str) -> str:
52
+ """
53
+ - reset the button
54
+ - prepend today's date (after the button)
55
+ """
56
+ lines = markdown.splitlines()
57
+ lines[0] = "- [ ] Mark as done"
58
+ line_to_prepend = f"- [[{my_note.note_name} ({datetime.date.today().isoformat()})]]"
59
+ lines.insert(1, line_to_prepend)
60
+ lines[-1] = lines[-1] + "\n"
61
+ return "\n".join(lines)
@@ -0,0 +1,53 @@
1
+ def on_start():
2
+ my_note.set_file_contents(f"""---
3
+ in_topic: {default_topic}
4
+ out_topic: {default_topic}/publish
5
+ message_to_send: "Hello :)"
6
+ ---
7
+ - [ ] Send message
8
+ - Instructions:
9
+ - press the button above to send `message_to_send` to `out_topic`
10
+ - (this can be observed via mosquitto_pub)
11
+ - send a message to `in_topic` and observe it listed below
12
+ - `out_topic` is read dynamically but `in_topic` is provided for informational purposes only
13
+ - ---
14
+ """)
15
+
16
+
17
+ def on_mqtt_message(topic, message):
18
+ my_note.append_datetimestamped_markdown_list_line(f"received on `{topic}`: {message}")
19
+
20
+
21
+ def on_note_modified():
22
+ if (maybe_note := my_note.note_if_markdown_starts_with_pressed_button()) is not None:
23
+ frontmatter, markdown = maybe_note
24
+ else:
25
+ logging.debug("Note modified but button not pressed")
26
+ return
27
+
28
+ if (out_topic := frontmatter.get("out_topic")) is None:
29
+ logging.warning("Button pressed but Frontmatter out_topic was empty")
30
+ return
31
+
32
+ if (message_to_send := frontmatter.get("message_to_send")) is None:
33
+ logging.warning("Button pressed but Frontmatter message_to_send was empty")
34
+ return
35
+
36
+ logging.info(f"[{note_name}] publishing {message_to_send} to mqtt")
37
+ mqtt.publish(out_topic, message_to_send)
38
+
39
+ logging.info(f"[{note_name}] resetting markdown...")
40
+
41
+ sleep(.25) # hack to prevent a race condition with Obsidian's display
42
+ my_note.upsert_markdown(lambda markdown: upserter(out_topic, message_to_send, markdown))
43
+ logging.info(f"[{note_name}] Markdown reset")
44
+
45
+
46
+ # util
47
+
48
+ def upserter(out_topic, message_to_send, markdown):
49
+ lines = markdown.splitlines()
50
+ lines[0] = f"- [ ] Send (last sent ~{ctime()})"
51
+ lines.append(timestamped_markdown_list_line(f"sending on `{out_topic}`: {message_to_send}"))
52
+
53
+ return "\n".join(lines)