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.
- nervous_notes-0.0.1/.gitignore +5 -0
- nervous_notes-0.0.1/LICENSE +19 -0
- nervous_notes-0.0.1/PKG-INFO +110 -0
- nervous_notes-0.0.1/README.md +89 -0
- nervous_notes-0.0.1/pyproject.toml +37 -0
- nervous_notes-0.0.1/src/nervous_notes/__init__.py +10 -0
- nervous_notes-0.0.1/src/nervous_notes/__main__.py +44 -0
- nervous_notes-0.0.1/src/nervous_notes/dsl.py +63 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Example - Daily Responsibility.py +75 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Example - Extrovert.py +34 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Example - Introvert.py +54 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Example - Recurring Note Generator.py +61 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Literate Programming - Hello World.py +53 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Mqtt Publisher.py +30 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Mqtt Subscriber.py +8 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Next Notes Manager.py +25 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Next-Notes MOC.py +91 -0
- nervous_notes-0.0.1/src/nervous_notes/example_scripts/Timer Demo.py +31 -0
- nervous_notes-0.0.1/src/nervous_notes/nervous_note.py +144 -0
- nervous_notes-0.0.1/src/nervous_notes/nervous_process.py +60 -0
- nervous_notes-0.0.1/src/nervous_notes/note_companion_script.py +190 -0
- nervous_notes-0.0.1/src/nervous_notes/note_embedded_script.py +112 -0
- nervous_notes-0.0.1/src/nervous_notes/orchestration.py +60 -0
- nervous_notes-0.0.1/src/nervous_notes/requirements.txt +5 -0
- nervous_notes-0.0.1/src/nervous_notes/util.py +105 -0
- nervous_notes-0.0.1/src/nervous_notes/vault_router.py +33 -0
- nervous_notes-0.0.1/src/nervous_notes/wrappers/external_messages.py +127 -0
- nervous_notes-0.0.1/src/nervous_notes/wrappers/file_watcher.py +145 -0
- nervous_notes-0.0.1/src/nervous_notes/wrappers/note_api.py +288 -0
- nervous_notes-0.0.1/src/nervous_notes/wrappers/scripting.py +156 -0
- nervous_notes-0.0.1/tests/experimental_test.py +211 -0
- nervous_notes-0.0.1/tests/nextnotes_moc_test.py +120 -0
- nervous_notes-0.0.1/tests/utils.py +96 -0
|
@@ -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)
|