diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..418724a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: macOS Tests + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + pip install . + - name: Lint + run: | + ruff check . + black --check . + - name: Test + run: pytest -q diff --git a/pyproject.toml b/pyproject.toml index 9f3ee14..3c145e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,10 @@ package-mode = true include = ["src/nodetool/package_metadata/nodetool-apple.json"] repository = "https://github.com/nodetool-ai/nodetool-apple" +[tool.ruff] +[tool.ruff.lint] +extend-ignore = ["F401", "F841"] + [tool.poetry.dependencies] python = "^3.11" nodetool-core = { git = "https://github.com/nodetool-ai/nodetool-core.git", rev = "main" } diff --git a/src/nodetool/nodes/apple/dictionary.py b/src/nodetool/nodes/apple/dictionary.py index 33d02f8..2aa8cff 100644 --- a/src/nodetool/nodes/apple/dictionary.py +++ b/src/nodetool/nodes/apple/dictionary.py @@ -31,7 +31,9 @@ def is_cacheable(cls) -> bool: async def process(self, context: ProcessingContext) -> list[str]: if not IS_MACOS: - raise NotImplementedError("Dictionary functionality is only available on macOS") + raise NotImplementedError( + "Dictionary functionality is only available on macOS" + ) if not self.term: return [] diff --git a/src/nodetool/nodes/apple/messages.py b/src/nodetool/nodes/apple/messages.py index 53be211..5b30eef 100644 --- a/src/nodetool/nodes/apple/messages.py +++ b/src/nodetool/nodes/apple/messages.py @@ -30,7 +30,9 @@ def is_cacheable(cls) -> bool: async def process(self, context: ProcessingContext): if not IS_MACOS: - raise NotImplementedError("Messages functionality is only available on macOS") + raise NotImplementedError( + "Messages functionality is only available on macOS" + ) text_content = escape_for_applescript(self.text) recipient = escape_for_applescript(self.recipient) diff --git a/src/nodetool/nodes/apple/notes.py b/src/nodetool/nodes/apple/notes.py index 263de8a..7e9b2b2 100644 --- a/src/nodetool/nodes/apple/notes.py +++ b/src/nodetool/nodes/apple/notes.py @@ -13,6 +13,7 @@ export_notes_script = Path(__file__).parent / "exportnotes.applescript" + def escape_for_applescript(text: str) -> str: """Escape special characters for AppleScript strings.""" # First escape backslashes, then quotes @@ -33,6 +34,7 @@ class CreateNote(BaseNode): - Create documentation or records - Save workflow outputs as notes """ + title: str = Field(default="", description="Title of the note") body: str = Field(default="", description="Content of the note") folder: str = Field(default="Notes", description="Notes folder to save to") @@ -68,10 +70,14 @@ async def process(self, context: ProcessingContext): except subprocess.CalledProcessError as e: raise Exception(f"Failed to create note: {e.stderr}") + class ReadNotes(BaseNode): """Read notes from Apple Notes via AppleScript""" + note_limit: int = Field(default=10, description="Maximum number of notes to export") - note_limit_per_folder: int = Field(default=10, description="Maximum notes per folder") + note_limit_per_folder: int = Field( + default=10, description="Maximum notes per folder" + ) @classmethod def is_cacheable(cls) -> bool: diff --git a/src/nodetool/nodes/apple/screen.py b/src/nodetool/nodes/apple/screen.py index 07024cb..7ac0c3e 100644 --- a/src/nodetool/nodes/apple/screen.py +++ b/src/nodetool/nodes/apple/screen.py @@ -24,7 +24,9 @@ class CaptureScreen(BaseNode): async def process(self, context: ProcessingContext) -> ImageRef: if not IS_MACOS: - raise NotImplementedError("Screen capture functionality is only available on macOS") + raise NotImplementedError( + "Screen capture functionality is only available on macOS" + ) main_display = Quartz.CGMainDisplayID() # type: ignore # If region is specified, capture that region, otherwise capture full screen diff --git a/tests/test_escape_for_applescript.py b/tests/test_escape_for_applescript.py new file mode 100644 index 0000000..8ddf5f6 --- /dev/null +++ b/tests/test_escape_for_applescript.py @@ -0,0 +1,12 @@ +from nodetool.nodes.apple.notes import escape_for_applescript + + +def test_escape_quotes_and_backslashes(): + text = 'Hello "World" \\ Test' + expected = 'Hello \\"World\\" \\\\ Test' + assert escape_for_applescript(text) == expected + + +def test_escape_newlines(): + text = "line1\nline2" + assert escape_for_applescript(text) == "line1\\nline2" diff --git a/tests/test_nodes_cacheable.py b/tests/test_nodes_cacheable.py new file mode 100644 index 0000000..dd8c8ae --- /dev/null +++ b/tests/test_nodes_cacheable.py @@ -0,0 +1,17 @@ +import pytest +from nodetool.nodes.apple import calendar, notes, messages, speech, dictionary + +CASES = [ + (calendar.CreateCalendarEvent, False), + (calendar.ListCalendarEvents, False), + (notes.CreateNote, False), + (notes.ReadNotes, False), + (messages.SendMessage, False), + (speech.SayText, False), + (dictionary.SearchDictionary, True), +] + + +@pytest.mark.parametrize("node_cls,expected", CASES) +def test_node_is_cacheable(node_cls, expected): + assert node_cls.is_cacheable() is expected