nervous-notes 0.0.2__tar.gz → 0.0.3__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 (34) hide show
  1. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/PKG-INFO +18 -5
  2. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/README.md +17 -4
  3. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/pyproject.toml +3 -2
  4. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/dsl.py +5 -0
  5. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Next-Notes MOC.py +0 -1
  6. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/nervous_note.py +30 -10
  7. nervous_notes-0.0.3/src/nervous_notes/nervous_note_router.py +50 -0
  8. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/note_companion_script.py +28 -21
  9. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/note_embedded_script.py +3 -3
  10. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/orchestration.py +1 -1
  11. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/wrappers/external_messages.py +7 -2
  12. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/wrappers/note_api.py +0 -2
  13. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/wrappers/scripting.py +18 -6
  14. nervous_notes-0.0.2/src/nervous_notes/vault_router.py +0 -33
  15. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/.gitignore +0 -0
  16. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/LICENSE +0 -0
  17. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/__init__.py +0 -0
  18. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/__main__.py +0 -0
  19. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Example - Daily Responsibility.py +0 -0
  20. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Example - Extrovert.py +0 -0
  21. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Example - Introvert.py +0 -0
  22. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Example - Recurring Note Generator.py +0 -0
  23. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Literate Programming - Hello World.py +0 -0
  24. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Mqtt Publisher.py +0 -0
  25. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Mqtt Subscriber.py +0 -0
  26. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Next Notes Manager.py +0 -0
  27. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/example_scripts/Timer Demo.py +0 -0
  28. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/nervous_process.py +0 -0
  29. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/requirements.txt +0 -0
  30. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/util.py +0 -0
  31. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/src/nervous_notes/wrappers/file_watcher.py +0 -0
  32. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/tests/experimental_test.py +0 -0
  33. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/tests/nextnotes_moc_test.py +0 -0
  34. {nervous_notes-0.0.2 → nervous_notes-0.0.3}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nervous-notes
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Personal knowledge management meets the actor model
5
5
  Project-URL: Homepage, https://github.com/micseydel/nervous_notes
6
6
  Project-URL: Issues, https://github.com/micseydel/nervous_notes/issues
@@ -53,6 +53,16 @@ this_is: yaml
53
53
 
54
54
  FIXME `--library`
55
55
 
56
+
57
+ ### Companion scripts and the DSL
58
+
59
+ - on_start
60
+ - on_note_modified
61
+ - on_mqtt_message
62
+ - on_timer
63
+ - on_message (or is it on_other_message? different from on_receive)
64
+ - on_stop
65
+
56
66
  ## Glossary
57
67
 
58
68
  - FIXME
@@ -73,10 +83,6 @@ FIXME `--library`
73
83
 
74
84
  - FIXME (placeholder)
75
85
 
76
- ## Polyglot programming
77
-
78
- This module currently includes mqtt support, which allows for sending messages between programs in different languages.
79
-
80
86
  ## Knowledge Based Programming
81
87
 
82
88
  Again, from the actor model paper:
@@ -103,8 +109,10 @@ I originally implemented this in Scala, where I called it "[woke notes](https://
103
109
 
104
110
  ## Future work
105
111
 
112
+ - remove all FIXMEs from the code
106
113
  - 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)
107
114
  - maybe a "whisper" optional dependency for AI transcription (since this was one of my primary use-cases)
115
+ - ntfy doesn't need to be core either, so `requests` could similarly be made an optional dependency
108
116
  - similar modules for other languages/platforms (e.g. JVM, Javascript, Go, etc.) to improve polyglot support
109
117
  - is Obsidian Canvas rich enough to wire actors together into flows?
110
118
  - improved isolation between companion notes
@@ -112,3 +120,8 @@ I originally implemented this in Scala, where I called it "[woke notes](https://
112
120
  - replace Pykka with something more Akka-like
113
121
  - this would likely be an _enormous_ undertaking
114
122
  - support actor-scoped `--library` imports, so that hot-reloading of imports can work
123
+ - Change .wake() class method design
124
+ - Don't bother trying for a uniform interface with pykka anymore
125
+ - ...because wake()ing is idempotent, has a different semantic
126
+ - Jumping to the wake() implementation is confusing
127
+ - Composition instead of inheritance?
@@ -32,6 +32,16 @@ this_is: yaml
32
32
 
33
33
  FIXME `--library`
34
34
 
35
+
36
+ ### Companion scripts and the DSL
37
+
38
+ - on_start
39
+ - on_note_modified
40
+ - on_mqtt_message
41
+ - on_timer
42
+ - on_message (or is it on_other_message? different from on_receive)
43
+ - on_stop
44
+
35
45
  ## Glossary
36
46
 
37
47
  - FIXME
@@ -52,10 +62,6 @@ FIXME `--library`
52
62
 
53
63
  - FIXME (placeholder)
54
64
 
55
- ## Polyglot programming
56
-
57
- This module currently includes mqtt support, which allows for sending messages between programs in different languages.
58
-
59
65
  ## Knowledge Based Programming
60
66
 
61
67
  Again, from the actor model paper:
@@ -82,8 +88,10 @@ I originally implemented this in Scala, where I called it "[woke notes](https://
82
88
 
83
89
  ## Future work
84
90
 
91
+ - remove all FIXMEs from the code
85
92
  - 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)
86
93
  - maybe a "whisper" optional dependency for AI transcription (since this was one of my primary use-cases)
94
+ - ntfy doesn't need to be core either, so `requests` could similarly be made an optional dependency
87
95
  - similar modules for other languages/platforms (e.g. JVM, Javascript, Go, etc.) to improve polyglot support
88
96
  - is Obsidian Canvas rich enough to wire actors together into flows?
89
97
  - improved isolation between companion notes
@@ -91,3 +99,8 @@ I originally implemented this in Scala, where I called it "[woke notes](https://
91
99
  - replace Pykka with something more Akka-like
92
100
  - this would likely be an _enormous_ undertaking
93
101
  - support actor-scoped `--library` imports, so that hot-reloading of imports can work
102
+ - Change .wake() class method design
103
+ - Don't bother trying for a uniform interface with pykka anymore
104
+ - ...because wake()ing is idempotent, has a different semantic
105
+ - Jumping to the wake() implementation is confusing
106
+ - Composition instead of inheritance?
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nervous-notes"
7
- version = "0.0.2"
7
+ version = "0.0.3"
8
8
  authors = [
9
9
  { name="Michael Seydel", email="contact@micseydel.me" },
10
10
  ]
@@ -20,8 +20,9 @@ license-files = ["LICEN[CS]E*"]
20
20
  dependencies = [
21
21
  "pykka==4.4.2",
22
22
  "ruamel.yaml==0.19.1",
23
- "paho-mqtt==2.1.0",
24
23
  "watchdog==6.0.0",
24
+ # FIXME: make these optional
25
+ "paho-mqtt==2.1.0",
25
26
  "requests==2.34.2",
26
27
  ]
27
28
 
@@ -9,6 +9,7 @@ import json
9
9
  import datetime
10
10
  import re
11
11
  from time import ctime, sleep
12
+ from typing import Any
12
13
 
13
14
  import requests
14
15
  from pykka import ActorRef
@@ -62,3 +63,7 @@ def on_timer(key: str, payload: object) -> None: pass
62
63
 
63
64
 
64
65
  def publish_to_ntfy(channel: str, message: str) -> None: pass
66
+
67
+
68
+ def batch_publish_to_nervous_note(note_id: str, messages: list[Any]) -> None:
69
+ pass
@@ -26,7 +26,6 @@ default_interval_days: {DEFAULT_INTERVAL_DAYS}
26
26
 
27
27
 
28
28
  def on_note_modified():
29
- logging.info("[Next-Notes Moc.py:on_note_modified] CANARY")
30
29
  note = my_note.note_if_markdown_starts_with_pressed_button()
31
30
  if note:
32
31
  frontmatter, markdown = note
@@ -1,12 +1,13 @@
1
- import logging
2
1
  import os
2
+ import logging
3
+ import threading
3
4
  from typing import TypeVar, Any, ClassVar
4
5
 
5
6
  import pykka
6
7
  from pykka import Actor, ActorRef
7
8
  from watchdog.events import FileModifiedEvent, FileCreatedEvent
8
9
 
9
- from .vault_router import VaultRouter
10
+ from .nervous_note_router import NervousNoteRouter, MessageBatch, NervousNoteSubscription
10
11
  from .wrappers.note_api import NoteAPI, PrimitiveNoteAPI
11
12
  from .wrappers.external_messages import MqttPublish, MqttSubscription, MqttConfig, ExternalMessages
12
13
  from .wrappers.file_watcher import VaultWatcher, VaultNoteSubscription
@@ -32,7 +33,10 @@ class NervousNoteSupport:
32
33
  vault_name: str = None
33
34
  file_path = None
34
35
  external_messages_actor_ref = None
35
- vault_router = None
36
+ nervous_note_router = None
37
+
38
+ actor_cache: dict[str, ActorRef] = {}
39
+ lock = threading.Lock()
36
40
 
37
41
  def __init__(self, vault_path: str, mqtt_config: MqttConfig):
38
42
  self.vault_path = vault_path
@@ -41,12 +45,24 @@ class NervousNoteSupport:
41
45
  self.vault_watcher = VaultWatcher.start(self.vault_path)
42
46
  self.file_path = os.path.join(self.vault_path, "Nervous Notes Mqtt Orchestrator.md")
43
47
  self.external_messages_actor_ref = ExternalMessages.start(mqtt_config, self.file_path)
44
- self.vault_router = VaultRouter.start()
48
+ self.nervous_note_router = NervousNoteRouter.start()
49
+
50
+ def wake(self, cls, note_name: str, *args: Any, **kwargs: Any):
51
+ with self.lock:
52
+ if note_name in self.actor_cache:
53
+ logging.warning(f"[nervous_note.wake] Fetching {note_name} from cache, ignoring *args and **kwargs: {args} {kwargs}")
54
+ # FIXME: send *args and **kwargs to the actor to let it know about the double-wake?
55
+ # should also document/rename wake being different from start() (which also answers the uniform interface question)
56
+ return self.actor_cache[note_name]
57
+ else:
58
+ actor_ref = cls.start(note_name, *args, **kwargs)
59
+ self.actor_cache[note_name] = actor_ref
60
+ return actor_ref
45
61
 
46
62
  def stop(self):
47
63
  self.vault_watcher.stop()
48
64
  self.external_messages_actor_ref.stop()
49
- self.vault_router.stop()
65
+ self.nervous_note_router.stop()
50
66
 
51
67
 
52
68
  class NervousNote(pykka.ThreadingActor):
@@ -58,7 +74,8 @@ class NervousNote(pykka.ThreadingActor):
58
74
  vault_path: str,
59
75
  mqtt_config: MqttConfig,
60
76
  ):
61
- assert cls.support is None, "Background actors already started" # FIXME: make this idempotent?
77
+ # FIXME: make this idempotent? probably not, but document in a [[Design Decisions]] note
78
+ assert cls.support is None, "Background actors already started"
62
79
  cls.support = NervousNoteSupport(vault_path, mqtt_config)
63
80
 
64
81
  @classmethod
@@ -77,15 +94,18 @@ class NervousNote(pykka.ThreadingActor):
77
94
  ) -> ActorRef[Actor]:
78
95
  assert cls.support is not None, "Did you run NervousNote.start_background_actors(vault_path, mqtt_config)?"
79
96
 
80
- # .start() comes from pykka.ThreadingActor
81
- actor_ref = cls.start(note_name,
82
- # e.g. this is where e.g. ScriptedNote.scripts_dir gets passed through
83
- *args, **kwargs)
97
+ # idempotency - grab from cache if existing
98
+ actor_ref = cls.support.wake(cls, note_name,
99
+ # e.g. this is where e.g. ScriptedNote.scripts_dir gets passed through
100
+ *args, **kwargs)
84
101
 
85
102
  # a NervousNote adds events from mqtt and note updates
86
103
  # FIXME: ...and timers??!
104
+ # FIXME: this is more convenient for a batch publisher anyway since the default_topic is more complicated than the note name
87
105
  cls.support.vault_watcher.tell(VaultNoteSubscription(note_name, actor_ref))
88
106
 
107
+ cls.support.nervous_note_router.tell(NervousNoteSubscription(note_name, actor_ref))
108
+
89
109
  default_topic = _default_topic(cls.support.vault_name, note_name)
90
110
  cls.support.external_messages_actor_ref.tell(MqttSubscription(default_topic, actor_ref))
91
111
 
@@ -0,0 +1,50 @@
1
+ import logging
2
+ from collections import defaultdict
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from pykka import ActorRef, ThreadingActor
7
+
8
+
9
+ @dataclass
10
+ class NervousNoteSubscription:
11
+ note_id: str
12
+ subscriber: ActorRef
13
+
14
+
15
+ @dataclass
16
+ class PublishToNervousNote:
17
+ note_id: str # important for snoopers
18
+ payload: Any
19
+
20
+
21
+ @dataclass
22
+ class MessageBatch:
23
+ messages: list[Any]
24
+
25
+
26
+ @dataclass
27
+ class BatchPublishToNervousNote:
28
+ note_id: str # important for snoopers
29
+ messages: MessageBatch
30
+
31
+ @staticmethod
32
+ def of(note_id: str, messages: list[Any]):
33
+ return BatchPublishToNervousNote(note_id, MessageBatch(messages))
34
+
35
+
36
+ class NervousNoteRouter(ThreadingActor):
37
+ def __init__(self):
38
+ super().__init__()
39
+ # FIXME since I'm changing things up anyway, a NervousNoteRef (or whatever) should probably include note_id
40
+ self.subscribers: dict[str, set[ActorRef]] = defaultdict(set)
41
+
42
+ def on_receive(self, message):
43
+ if isinstance(message, NervousNoteSubscription):
44
+ # a NervousNote subscribes to its own [[Note Name]] by default, but we allow for snoopers (for now)
45
+ self.subscribers[message.note_id].add(message.subscriber)
46
+ elif isinstance(message, (PublishToNervousNote, BatchPublishToNervousNote)):
47
+ for subscriber in self.subscribers[message.note_id]:
48
+ subscriber.tell(message)
49
+ else:
50
+ logging.warning(f"Unknown message type: {type(message)}")
@@ -8,6 +8,7 @@ from pykka import ActorRef, ActorDeadError
8
8
  from watchdog.events import FileModifiedEvent, FileCreatedEvent
9
9
 
10
10
  from .nervous_note import NervousNote, MqttWrapper
11
+ from .nervous_note_router import BatchPublishToNervousNote, PublishToNervousNote
11
12
  from .wrappers.note_api import NoteAPI
12
13
  from .wrappers.scripting import ScriptHarness, ScriptPath
13
14
  from .wrappers.external_messages import MqttPublish
@@ -33,7 +34,7 @@ class NoteCompanionScript(NervousNote):
33
34
  self.script_wrapper = ScriptedNoteHarness(
34
35
  self.actor_ref, self.my_note,
35
36
  self.topic, self.mqtt,
36
- self.support.vault_router,
37
+ self.support.nervous_note_router,
37
38
  self.script_path,
38
39
  )
39
40
  self.script_wrapper.on_start()
@@ -53,6 +54,9 @@ class NoteCompanionScript(NervousNote):
53
54
  elif message == "SCRIPT_MODIFIED":
54
55
  logging.info(f"Hot reloading [[{self.note_name}]]")
55
56
  self.script_wrapper.compile_script(this_is_a_recompile=True)
57
+ elif isinstance(message, (PublishToNervousNote, BatchPublishToNervousNote)):
58
+ # FIXME: worth renaming script_wrapper.on_other_message if it's really this specific?
59
+ self.script_wrapper.on_other_message(message)
56
60
  else:
57
61
  try:
58
62
  message_type, key, payload = message
@@ -117,6 +121,7 @@ class NoteCompanionScriptsManager(NervousNote):
117
121
  logging.warning(f"{maybe_note_name} was not in {list(self.nervous_notes.keys())}")
118
122
  raise
119
123
  except ActorDeadError:
124
+ # FIXME: do I need to log the exception here, or just an error?
120
125
  logging.exception(f"{maybe_note_name} is dead, could not send a FileModifiedEvent to it")
121
126
  logging.warning(f"NOT HOT RELOADING {message.src_path}") # FIXME - it DOES get hotreloaded, doesn't it?
122
127
  else:
@@ -130,21 +135,23 @@ class NoteCompanionScriptsManager(NervousNote):
130
135
  if message.src_path.startswith(self.support.vault_path):
131
136
  pass # FIXME - on_note_created event?
132
137
  elif message.src_path.startswith(self.scripts_dir):
133
- script_path_to_wake = message.src_path
134
- note_name: str = os.path.splitext(os.path.split(script_path_to_wake)[1])[0]
135
- if note_name in self.nervous_notes:
136
- logging.warning(f"Got a file created event for script {script_path_to_wake} but it was already created (ignoring)")
137
- else:
138
- logging.info(f"Detected new script {script_path_to_wake}, loading it and adding it for hotreloading...")
139
- actor_ref = NoteCompanionScript.wake(
140
- note_name,
141
- ScriptPath(self.scripts_dir, note_name)
142
- )
143
- self.nervous_notes[note_name] = actor_ref
144
- self.__update_note()
138
+ # FIXME: gate starting new .py scripts behind [[Note Companion Scripts Manager]]
139
+ logging.warning(f"Ignoring {message.src_path}, restart the app to load the new script")
140
+ # script_path_to_wake = message.src_path
141
+ # note_name: str = os.path.splitext(os.path.split(script_path_to_wake)[1])[0]
142
+ # if note_name in self.nervous_notes:
143
+ # logging.warning(f"Got a file created event for script {script_path_to_wake} but it was already created (ignoring)")
144
+ # else:
145
+ # logging.info(f"Detected new script {script_path_to_wake}, loading it and adding it for hotreloading...")
146
+ # actor_ref = NoteCompanionScript.wake(
147
+ # note_name,
148
+ # ScriptPath(self.scripts_dir, note_name)
149
+ # )
150
+ # self.nervous_notes[note_name] = actor_ref
151
+ # self.__update_note()
145
152
  else:
146
- logging.warning(
147
- f"unexpected new path `{message.src_path}` (did not start with scripts dir `{self.scripts_dir}` or vault dir `{self.support.vault_path}`)")
153
+ logging.warning(f"unexpected new path `{message.src_path}` (did not start with scripts dir `{self.scripts_dir}` or "
154
+ f"vault dir `{self.support.vault_path}`)")
148
155
  elif isinstance(message, MqttPublish):
149
156
  self.on_mqtt_message(message.topic, message.payload)
150
157
  else:
@@ -172,22 +179,22 @@ class NoteCompanionScriptsManager(NervousNote):
172
179
 
173
180
 
174
181
  class ScriptedNoteHarness(ScriptHarness):
175
- def __init__(self, actor_ref: ActorRef, my_note: NoteAPI, topic: str, mqtt: MqttWrapper, vault_router: ActorRef, script_path: ScriptPath):
176
- super().__init__(actor_ref, my_note, topic, mqtt, vault_router, script_path)
182
+ def __init__(self, actor_ref: ActorRef, my_note: NoteAPI, topic: str, mqtt: MqttWrapper, nervous_note_router: ActorRef, script_path: ScriptPath):
183
+ super().__init__(actor_ref, my_note, topic, mqtt, nervous_note_router, script_path)
177
184
  self._scripts = []
178
- self.script_scope["wake"] = self.__wake_subordinate
185
+ self.script_scope["wake"] = self.__wake
179
186
  self.script_scope["TemplateScript"] = TemplateScript
180
187
 
181
- def __wake_subordinate(self, filename: str, template_script: TemplateScript) -> ActorRef:
188
+ def __wake(self, filename: str, template_script: TemplateScript) -> ActorRef:
182
189
  if isinstance(template_script, TemplateScript):
183
- script = NoteCompanionScript.wake( # FIXME: fk wake->start if possible!
190
+ script = NoteCompanionScript.wake(
184
191
  filename,
185
192
  ScriptPath(os.path.join(self.script_path.scripts_dir, "templates"), template_script.script_name)
186
193
  )
187
194
  self._scripts.append(script)
188
195
  return script
189
196
  else:
190
- raise NotImplementedError
197
+ raise NotImplementedError(f"Only TemplateScript templates are supported currently")
191
198
 
192
199
  def get_script(self) -> str:
193
200
  with open(self.script_path.script_path) as f:
@@ -53,7 +53,7 @@ class NoteEmbeddedScript(NervousNote):
53
53
 
54
54
  self.script_wrapper = NoteEmbeddedScriptHarness(self.actor_ref, self.my_note,
55
55
  self.topic, self.mqtt,
56
- self.support.vault_router)
56
+ self.support.nervous_note_router)
57
57
  self.script_wrapper.on_start()
58
58
 
59
59
  def on_receive(self, message):
@@ -85,8 +85,8 @@ class NoteEmbeddedScript(NervousNote):
85
85
 
86
86
 
87
87
  class NoteEmbeddedScriptHarness(ScriptHarness):
88
- def __init__(self, actor_ref: ActorRef, my_note: NoteAPI, topic: str, mqtt: MqttWrapper, vault_router: ActorRef):
89
- super().__init__(actor_ref, my_note, topic, mqtt, vault_router, f"{my_note.note_path}#Code")
88
+ def __init__(self, actor_ref: ActorRef, my_note: NoteAPI, topic: str, mqtt: MqttWrapper, nervous_note_router: ActorRef):
89
+ super().__init__(actor_ref, my_note, topic, mqtt, nervous_note_router, f"{my_note.note_path}#Code")
90
90
 
91
91
  def get_script(self) -> str:
92
92
  with open(self.my_note.note_path) as f:
@@ -20,7 +20,7 @@ class Orchestrator(NervousNote):
20
20
  # this runs in the spawner's context, whereas on_start runs in the actor thread
21
21
  super().__init__(note_name, *args, **kwargs)
22
22
 
23
- self.scripts_dir = scripts_dir
23
+ self.scripts_dir = os.path.abspath(scripts_dir)
24
24
  self.libraries_dir = libraries_dir
25
25
  self.started = []
26
26
  self.deploy_debug_notes = deploy_debug_notes
@@ -86,6 +86,7 @@ class ExternalMessages(pykka.ThreadingActor):
86
86
  if isinstance(msg, MqttSubscription):
87
87
  if self.mqtt_client is not None:
88
88
  logging.debug(f"Subscribing to {msg.topic}: {msg.subscriber}")
89
+ # FIXME: how best to handle a user accidentally providing a list typed topic?
89
90
  self.mqtt_client.subscribe(msg.topic)
90
91
  self.subscribers[msg.topic].add(msg.subscriber)
91
92
  self.my_note.append(f"- \\[{ctime()}] subscribed to `{msg.topic}`: {msg.subscriber}\n")
@@ -93,8 +94,12 @@ class ExternalMessages(pykka.ThreadingActor):
93
94
  logging.warning(f"Ignoring subscription to {msg.topic}, no mqtt config")
94
95
  elif isinstance(msg, MqttPublish):
95
96
  if self.mqtt_client is not None:
96
- self.mqtt_client.publish(msg.topic, msg.payload)
97
- logging.debug(f"published {len(msg.payload)} bytes to `{msg.topic}`")
97
+ try:
98
+ self.mqtt_client.publish(msg.topic, msg.payload)
99
+ except Exception as e:
100
+ logging.exception(f"Failed to publish {msg.topic} ({type(msg.topic)}): {e}")
101
+ else:
102
+ logging.debug(f"published {len(msg.payload)} bytes to `{msg.topic}`")
98
103
  else:
99
104
  logging.warning(f"Ignoring publish to {msg.topic}, no mqtt config")
100
105
  else:
@@ -246,8 +246,6 @@ def _raw_note_to_frontmatter_and_markdown(raw_note_lines) -> Tuple[str | None, s
246
246
  frontmatter_lines.append(line)
247
247
 
248
248
  frontmatter = ''.join(frontmatter_lines)
249
- else:
250
- raise Exception("no end to frontmatter")
251
249
 
252
250
  markdown = ''.join(raw_note_lines) # remaining
253
251
  return frontmatter, markdown
@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING # FIXME: remove
12
12
  from pykka import ActorRef
13
13
 
14
14
  from ..util import Clock, TimeUtil
15
- from ..vault_router import Subscribe, Publish
15
+ from ..nervous_note_router import NervousNoteSubscription, PublishToNervousNote, BatchPublishToNervousNote
16
16
  from ..nervous_note import MqttWrapper
17
17
  from ..wrappers.note_api import NoteAPI, datetimestamped_markdown_list_line, timestamped_markdown_list_line
18
18
 
@@ -34,7 +34,7 @@ class ScriptPath:
34
34
  class ScriptHarness:
35
35
  my_note: NoteAPI
36
36
 
37
- def __init__(self, actor_ref: ActorRef, my_note: NoteAPI, topic: str, mqtt: MqttWrapper, vault_router: ActorRef, script_path: ScriptPath):
37
+ def __init__(self, actor_ref: ActorRef, my_note: NoteAPI, topic: str, mqtt: MqttWrapper, nervous_note_router: ActorRef, script_path: ScriptPath):
38
38
  self.my_note = my_note
39
39
  self.script_path = script_path
40
40
  self.actor_ref = actor_ref
@@ -64,8 +64,9 @@ class ScriptHarness:
64
64
  "cancel_timer": self.cancel_timer,
65
65
  "datetimestamped_markdown_list_line": datetimestamped_markdown_list_line,
66
66
  "timestamped_markdown_list_line": timestamped_markdown_list_line,
67
- "subscribe_internal": lambda t, s: vault_router.tell(Subscribe(t, s)),
68
- "publish_internal": lambda t, p: vault_router.tell(Publish(t, p)),
67
+ # "snoop_on_nervous_note": lambda nid, s: nervous_note_router.tell(NervousNoteSubscription(nid, s)),
68
+ "publish_to_nervous_note": lambda nid, p: nervous_note_router.tell(PublishToNervousNote(nid, p)),
69
+ "batch_publish_to_nervous_note": lambda nid, batch: nervous_note_router.tell(BatchPublishToNervousNote.of(nid, batch)),
69
70
  "publish_to_ntfy": publish_to_ntfy,
70
71
  "next_occurrence": timeutil.next_occurrence,
71
72
  "seconds_until": timeutil.seconds_until,
@@ -78,6 +79,7 @@ class ScriptHarness:
78
79
  "on_start": lambda: None,
79
80
  "on_timer": lambda key, payload: None,
80
81
  "on_stop": lambda: None,
82
+ "on_other_message": lambda: None,
81
83
  }
82
84
 
83
85
  self.prior_script = None
@@ -120,8 +122,12 @@ class ScriptHarness:
120
122
  else:
121
123
  logging.info(f"{self.script_path} compilation complete, executing to update the scope now...")
122
124
  # this may update the scope with potentially new on* event functions
123
- exec(self.compiled_script, self.script_scope)
124
- self.prior_script = script
125
+ try:
126
+ exec(self.compiled_script, self.script_scope)
127
+ except Exception as e:
128
+ logging.exception(f"Failed to compile {self.script_path} ({e})")
129
+ else:
130
+ self.prior_script = script
125
131
 
126
132
  def on_start(self) -> None:
127
133
  try:
@@ -151,6 +157,12 @@ class ScriptHarness:
151
157
  def on_stop(self) -> None:
152
158
  logging.info(f"stopping ScriptHarness")
153
159
 
160
+ def on_other_message(self, message) -> None:
161
+ try:
162
+ self.script_scope["on_other_message"](message)
163
+ except Exception as e:
164
+ logging.exception(f"on_other_message call failed {e}")
165
+
154
166
 
155
167
  # FIXME: move to "services" or something, with weather, PurpleAir, etc
156
168
  def publish_to_ntfy(channel: str, message: str) -> None:
@@ -1,33 +0,0 @@
1
- import logging
2
- from collections import defaultdict
3
- from dataclasses import dataclass
4
- from typing import Set, Dict
5
-
6
- from pykka import ActorRef, ThreadingActor
7
-
8
-
9
- @dataclass
10
- class Subscribe:
11
- topic: str
12
- subscriber: ActorRef
13
-
14
-
15
- @dataclass
16
- class Publish:
17
- topic: str
18
- payload: object
19
-
20
-
21
- class VaultRouter(ThreadingActor):
22
- def __init__(self):
23
- super().__init__()
24
- self.subscribers: Dict[str, Set[ActorRef]] = defaultdict(set)
25
-
26
- def on_message(self, message):
27
- if isinstance(message, Subscribe):
28
- self.subscribers[message.topic].add(message.subscriber)
29
- elif isinstance(message, Publish):
30
- for subscriber in self.subscribers[message.topic]:
31
- subscriber.tell(message.payload)
32
- else:
33
- logging.warning(f"Unknown message type: {type(message)}")
File without changes
File without changes