├── .flake8 ├── examples ├── img │ ├── chat.png │ └── zoom.png ├── simple_notification.py ├── notification_with_reply.py ├── notification_with_action.py ├── multiple_notifications_with_callbacks.py └── cancel_multiple_notifications.py ├── docs ├── img │ ├── example-run.gif │ ├── macos-notifications.png │ └── enable-notifications.png ├── code │ ├── listener_process.md │ ├── notification_sender.md │ ├── singleton.md │ ├── manager.md │ ├── notification_config.md │ └── client.md ├── user_guide.md ├── faq.md ├── release-notes.md ├── index.md └── examples.md ├── src └── mac_notifications │ ├── __init__.py │ ├── singleton.py │ ├── listener_process.py │ ├── client.py │ ├── notification_config.py │ ├── notification_sender.py │ └── manager.py ├── .github ├── mkdocs-requirements.txt └── workflows │ ├── publish-docs.yml │ ├── deploy-pypi.yml │ └── latest-changes.yml ├── CONTRIBUTING.md ├── LICENSE ├── .pre-commit-config.yaml ├── mkdocs.yml ├── pyproject.toml ├── README.md └── .gitignore /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=E203,W503 3 | max-line-length=120 4 | exclude = __init__.py 5 | -------------------------------------------------------------------------------- /examples/img/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jorricks/macos-notifications/HEAD/examples/img/chat.png -------------------------------------------------------------------------------- /examples/img/zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jorricks/macos-notifications/HEAD/examples/img/zoom.png -------------------------------------------------------------------------------- /docs/img/example-run.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jorricks/macos-notifications/HEAD/docs/img/example-run.gif -------------------------------------------------------------------------------- /docs/img/macos-notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jorricks/macos-notifications/HEAD/docs/img/macos-notifications.png -------------------------------------------------------------------------------- /docs/img/enable-notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jorricks/macos-notifications/HEAD/docs/img/enable-notifications.png -------------------------------------------------------------------------------- /docs/code/listener_process.md: -------------------------------------------------------------------------------- 1 | # Notification listener process 2 | 3 | The class doc explain it all :) 4 | 5 | ::: mac_notifications.listener_process.NotificationProcess -------------------------------------------------------------------------------- /docs/code/notification_sender.md: -------------------------------------------------------------------------------- 1 | # Notification sender 2 | 3 | This is the bread and butter of our application ⚡️! 4 | 5 | ::: mac_notifications.notification_sender.create_notification 6 | -------------------------------------------------------------------------------- /examples/simple_notification.py: -------------------------------------------------------------------------------- 1 | from mac_notifications import client 2 | 3 | 4 | if __name__ == "__main__": 5 | client.create_notification(title="Meeting starts now!", subtitle="Team Standup") 6 | -------------------------------------------------------------------------------- /src/mac_notifications/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple interactable Mac notifications with only pure-python dependencies 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | __version__ = "0.2.1" 8 | -------------------------------------------------------------------------------- /.github/mkdocs-requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.3.1 2 | mkdocs-autorefs==0.4.1 3 | mkdocs-material==8.3.9 4 | mkdocs-material-extensions==1.0.3 5 | mkdocstrings==0.19.0 6 | mkdocstrings-python==0.7.1 7 | termynal==0.2.0 8 | -------------------------------------------------------------------------------- /docs/code/singleton.md: -------------------------------------------------------------------------------- 1 | # Singleton 2 | 3 | A metaclass to ensure we don't instantiate this class twice. One object will be created at the start of our application. After this, it will always use this specific instance. 4 | 5 | ::: mac_notifications.singleton.Singleton 6 | -------------------------------------------------------------------------------- /docs/code/manager.md: -------------------------------------------------------------------------------- 1 | # Notification Manager 2 | 3 | The module that is responsible for managing the notifications over time & enabling callbacks to be executed. 4 | 5 | ::: mac_notifications.manager.NotificationManager 6 | 7 | ::: mac_notifications.manager.CallbackExecutorThread 8 | -------------------------------------------------------------------------------- /docs/user_guide.md: -------------------------------------------------------------------------------- 1 | # User guide 2 | The goal of this library is to make it as easy to use as possible. 3 | This page will list the documentation of the client function. 4 | For examples, please check [the examples](macos-notifications/examples/). 5 | 6 | ::: mac_notifications.client.create_notification 7 | -------------------------------------------------------------------------------- /docs/code/notification_config.md: -------------------------------------------------------------------------------- 1 | # Notification config 2 | 3 | The dataclasses that represent a Notification configuration. 4 | The first one is for the main process, the second one is for the actual creation & waiting for callback process. 5 | 6 | ::: mac_notifications.notification_config.NotificationConfig 7 | 8 | ::: mac_notifications.notification_config.JSONNotificationConfig 9 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: publish-docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | deploy-on-macos: 8 | runs-on: macos-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-python@v4 12 | with: 13 | python-version: '3.8' 14 | - name: Install Flit 15 | run: pip install flit 16 | - name: Install Dependencies 17 | run: flit install --symlink --extras docs 18 | - run: mkdocs gh-deploy --force 19 | -------------------------------------------------------------------------------- /docs/code/client.md: -------------------------------------------------------------------------------- 1 | # The client 2 | 3 | When you are looking to integrate macos-notifications with your project, the `client` is the first place to look. 4 | If you have a service that periodically restarts, you might want to take a look at the `CacheClient`. 5 | 6 | ::: mac_notifications.client 7 | 8 | 9 | ## More advanced usages 10 | To get a bit more information about the amount of notifications that are still active, you can use the following function: 11 | 12 | ::: mac_notifications.client.get_notification_manager 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to macos-notifications 2 | 3 | As an open source project, macos-notifications welcomes contributions of many forms. 4 | This document summarizes the process. 5 | 6 | Examples of contributions include: 7 | 8 | * Code patches 9 | * Documentation improvements 10 | * Bug reports 11 | * Pull request reviews 12 | 13 | Contributions are managed using GitHub's Pull Requests. 14 | For a PR to be accepted: 15 | 16 | * All automated checks must pass 17 | * The changeset must have a reasonable patch test coverage 18 | -------------------------------------------------------------------------------- /src/mac_notifications/singleton.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from threading import Lock 4 | from typing import Any, Dict, Type 5 | 6 | 7 | class Singleton(type): 8 | """Credits go to https://www.linkedin.com/pulse/writing-thread-safe-singleton-class-python-saurabh-singh/""" 9 | 10 | _instances: Dict[Type, Any] = {} 11 | 12 | _lock: Lock = Lock() 13 | 14 | def __call__(cls, *args, **kwargs): 15 | with cls._lock: 16 | if cls not in cls._instances: 17 | instance = super().__call__(*args, **kwargs) 18 | cls._instances[cls] = instance 19 | return cls._instances[cls] 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build-n-publish: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Check out source-code repository 12 | uses: actions/checkout@v3 13 | 14 | - name: Set up Python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: "3.8" 18 | 19 | - name: Install Flit 20 | run: pip install flit 21 | - name: Publish 22 | run: flit publish 23 | env: 24 | FLIT_USERNAME: __token__ 25 | FLIT_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 26 | -------------------------------------------------------------------------------- /examples/notification_with_reply.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import timedelta 3 | from pathlib import Path 4 | 5 | from mac_notifications import client 6 | 7 | 8 | if __name__ == "__main__": 9 | print("You have to press the notification within 30 seconds for it to work.") 10 | client.create_notification( 11 | title="Cool notification", 12 | subtitle="Subtitle of the notification", 13 | text="Hello, I contain info", 14 | icon=Path(__file__).parent / "img" / "chat.png", 15 | delay=timedelta(milliseconds=500), 16 | reply_button_str="Reply to this notification", 17 | reply_callback=lambda reply: print(f"Replied {reply=}"), 18 | snooze_button_str="Or click me", 19 | ) 20 | time.sleep(30) 21 | client.stop_listening_for_callbacks() 22 | -------------------------------------------------------------------------------- /examples/notification_with_action.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import time 4 | from functools import partial 5 | from pathlib import Path 6 | 7 | from mac_notifications import client 8 | 9 | 10 | def join_a_meeting(conf_number: int | str) -> None: 11 | print(f"Joining meeting with conf_number='{conf_number}'.") 12 | 13 | 14 | if __name__ == "__main__": 15 | print("You have to press the notification within 30 seconds for it to work.") 16 | client.create_notification( 17 | title="Meeting starts now!", 18 | subtitle="Standup. Join please.", 19 | icon=Path(__file__).parent / "img" / "zoom.png", 20 | action_button_str="Join zoom meeting", 21 | action_callback=partial(join_a_meeting, conf_number="12345678"), 22 | ) 23 | time.sleep(30) 24 | client.stop_listening_for_callbacks() 25 | -------------------------------------------------------------------------------- /.github/workflows/latest-changes.yml: -------------------------------------------------------------------------------- 1 | name: Latest Changes 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - main 7 | types: 8 | - closed 9 | # For manually triggering it 10 | workflow_dispatch: 11 | inputs: 12 | number: 13 | description: PR number 14 | required: true 15 | 16 | jobs: 17 | latest-changes: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Check out source-code repository 21 | uses: actions/checkout@v2 22 | 23 | - name: Update docs/release-notes.md with newest PR that was merged. 24 | uses: docker://tiangolo/latest-changes:0.0.3 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | latest_changes_file: docs/release-notes.md 28 | latest_changes_header: '## Latest Changes\n\n' 29 | debug_logs: true 30 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | ???+ "Why did you create this library ?" 3 | 4 | I wanted a library that did not depend on any non-python tools (so you had to go around and install that). Instead, I wanted a library where you install the pip packages and you are done. 5 | Later I realised how hard it was to integrate correctly with PyOBJC. Also, I had a hard time finding any examples on how to easily integrate this in a non-blocking fashion with my tool. 6 | Hence, I figured I should set it up to be as user-friendly as possible and share it with the world ;)! 7 | 8 | 9 | ???+ "My notifications don't show up with the text I mentioned" 10 | 11 | Unfortunately, you first need to allow Python to create notifications. Instructions are similar to what can be found [here](https://www.zdnet.com/article/how-to-enable-app-store-notifications-on-macos-to-help-keep-your-software-up-to-date/). The application will be named 'Python'. 12 | 13 | 14 | Your settings should look similar to this: 15 | 16 | ![](img/enable-notifications.png) 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jorrick Sleijster 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/release-notes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## Latest changes 4 | 5 | ## 0.1.5 - 1st August 2022 6 | - 📝 Python example was missing __name__ filter. 7 | 8 | ## 0.1.4 - 1st August 2022 9 | - 🐛 Multiple notifications in the same thread gave exceptions. Now you can send as many notifications as you want. 10 | 11 | ## 0.1.3 - 31 July 2022 12 | - 📝 Out-dated python example on main page 13 | 14 | ## 0.1.2 - 26 July 2022 15 | - 📝 Documentation update 16 | - 📝 Remove requirements from docs 17 | - 📝 Update badges 18 | 19 | ## 0.1.1 - 26 July 2022 20 | - 📝 Fix broken image in documentation 21 | 22 | ## 0.1.0 - 24 July 2022 23 | - 🔧 Rename all references of mac-notifications to macos-notifications 24 | 25 | ## 0.0.1a1 - 24 July 2022 26 | - 🔧 Rename project as mac-notifications was taken on pypi 27 | 28 | ## Initial release: 0.0.1a0 - 23 July 2022 29 | - 🚀 Easy python interface. It's as simple as '`client.create_notification(title="Meeting starts now!", subtitle="Team Standup")`' 30 | - 💥 Ability to add action buttons with callbacks! 31 | - 📝 Ability to reply to notifications! 32 | - ⌚ Delayed notifications. 33 | - ⏱️ Automatically time out the notification listener. 34 | - 📦 Just two packages (which is really just one package) as a dependency 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/mac_notifications/listener_process.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from multiprocessing import Process, SimpleQueue 4 | 5 | from mac_notifications import notification_sender 6 | from mac_notifications.notification_config import JSONNotificationConfig 7 | 8 | 9 | class NotificationProcess(Process): 10 | """ 11 | This is a simple process to launch a notification in a separate process. 12 | 13 | Why you may ask? 14 | First, the way we need to launch a notification using a class, this class can only be instantiated once in a 15 | process. Hence, for simple notifications we create a new process and then immediately stop it after the notification 16 | was launched. 17 | Second, waiting for the user interaction with a notification is a blocking operation. 18 | Because it is a blocking operation, if we want to be able to receive any user interaction from the notification, 19 | without completely halting/freezing our main process, we need to open it in a background process. 20 | """ 21 | 22 | def __init__(self, notification_config: JSONNotificationConfig, queue: SimpleQueue | None): 23 | super().__init__() 24 | self.notification_config = notification_config 25 | self.queue = queue 26 | 27 | def run(self) -> None: 28 | notification_sender.create_notification(self.notification_config, self.queue).send() 29 | # on if any of the callbacks are provided, start the event loop (this will keep the program from stopping) 30 | -------------------------------------------------------------------------------- /examples/multiple_notifications_with_callbacks.py: -------------------------------------------------------------------------------- 1 | import time 2 | from pathlib import Path 3 | 4 | from mac_notifications import client 5 | 6 | 7 | if __name__ == "__main__": 8 | print("Sending meeting notification.") 9 | client.create_notification( 10 | title="Meeting starts now.", 11 | subtitle="Standup Data Team", 12 | icon=Path(__file__).parent / "img" / "zoom.png", 13 | action_button_str="Join zoom meeting", 14 | action_callback=lambda: print("Joining zoom meeting now."), 15 | ) 16 | 17 | time.sleep(5) 18 | print("Sending notification.") 19 | client.create_notification( 20 | title="Message from Henk", 21 | subtitle="Hey Dude, are we still meeting?", 22 | icon=Path(__file__).parent / "img" / "chat.png", 23 | reply_button_str="Reply to this notification", 24 | reply_callback=lambda reply: print(f"You replied: {reply}"), 25 | ) 26 | print("Yet another message.") 27 | client.create_notification( 28 | title="Message from Daniel", 29 | subtitle="How you doing?", 30 | icon=Path(__file__).parent / "img" / "chat.png", 31 | reply_button_str="Reply to this notification", 32 | reply_callback=lambda reply: print(f"You replied: {reply}"), 33 | ) 34 | 35 | print("Application will remain active until both notifications have been answered.") 36 | while client.get_notification_manager().get_active_running_notifications() > 0: 37 | time.sleep(1) 38 | client.stop_listening_for_callbacks() 39 | -------------------------------------------------------------------------------- /examples/cancel_multiple_notifications.py: -------------------------------------------------------------------------------- 1 | import time 2 | from pathlib import Path 3 | 4 | from mac_notifications import client 5 | from mac_notifications.client import Notification 6 | 7 | 8 | if __name__ == "__main__": 9 | print("Sending meeting notification.") 10 | 11 | sent: list[Notification] = [] 12 | 13 | def cancel_all(): 14 | for i in sent: 15 | i.cancel() 16 | 17 | sent.append(client.create_notification( 18 | title="Annoyed by notifications?", 19 | icon=Path(__file__).parent / "img" / "zoom.png", 20 | action_button_str="Cancel all notifications", 21 | action_callback=cancel_all, 22 | )) 23 | 24 | time.sleep(5) 25 | print("Sending notification.") 26 | sent.append(client.create_notification( 27 | title="Message from Henk", 28 | subtitle="Hey Dude, are we still meeting?", 29 | icon=Path(__file__).parent / "img" / "chat.png", 30 | reply_button_str="Reply to this notification", 31 | reply_callback=lambda reply: print(f"You replied to Henk: {reply}"), 32 | )) 33 | 34 | print("Yet another message.") 35 | sent.append(client.create_notification( 36 | title="Message from Daniel", 37 | subtitle="How you doing?", 38 | icon=Path(__file__).parent / "img" / "chat.png", 39 | reply_button_str="Reply to this notification", 40 | reply_callback=lambda reply: print(f"You replied to Daniel: {reply}"), 41 | )) 42 | time.sleep(5) 43 | 44 | for i in sent: 45 | i.cancel() 46 | time.sleep(5) 47 | 48 | print("Application will remain active until both notifications have been answered.") 49 | while client.get_notification_manager().get_active_running_notifications() > 0: 50 | time.sleep(1) 51 | print("all notifications are handled") 52 | client.stop_listening_for_callbacks() 53 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # This file contains the [pre-commit](https://pre-commit.com/) configuration of this repository. 2 | # More on which specific pre-commit hooks we use can be found in README.md. 3 | --- 4 | minimum_pre_commit_version: "2.9.2" 5 | repos: 6 | - repo: meta 7 | hooks: 8 | - id: identity 9 | - id: check-hooks-apply 10 | - repo: local 11 | hooks: 12 | - id: isort 13 | name: iSort - Sorts imports. 14 | description: Sorts your import for you. 15 | entry: isort 16 | language: python 17 | types: 18 | - python 19 | require_serial: true 20 | exclude: examples/* 21 | additional_dependencies: 22 | - isort==5.10.1 23 | - id: black 24 | name: Black - Auto-formatter. 25 | description: Black is the uncompromising Python code formatter. Writing to files. 26 | entry: black 27 | language: python 28 | types: 29 | - python 30 | require_serial: true 31 | additional_dependencies: 32 | - black==22.6.0 33 | - id: flake8 34 | name: Flake8 - Enforce code style and doc. 35 | description: A command-line utility for enforcing style consistency across Python projects. 36 | entry: flake8 37 | args: 38 | - --config=.flake8 39 | language: python 40 | types: 41 | - python 42 | require_serial: true 43 | additional_dependencies: 44 | - flake8==4.0.1 45 | - id: mypy 46 | name: mypy - Static type checking 47 | description: Mypy helps ensure that we use our functions and variables correctly by checking the types. 48 | entry: mypy 49 | args: 50 | - --ignore-missing-imports 51 | - --scripts-are-modules 52 | language: python 53 | types: 54 | - python 55 | require_serial: true 56 | additional_dependencies: 57 | - mypy==0.961 58 | -------------------------------------------------------------------------------- /src/mac_notifications/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from datetime import timedelta 5 | from pathlib import Path 6 | from typing import Callable 7 | 8 | from mac_notifications.manager import Notification, NotificationManager 9 | from mac_notifications.notification_config import NotificationConfig 10 | 11 | """ 12 | This serves as the entrypoint for our users. They should only need to use this file. 13 | """ 14 | 15 | logging.basicConfig(level=logging.INFO) 16 | logger = logging.getLogger() 17 | 18 | 19 | def get_notification_manager() -> NotificationManager: 20 | """Return the NotificationManager object.""" 21 | return NotificationManager() 22 | 23 | 24 | def stop_listening_for_callbacks() -> None: 25 | return get_notification_manager().cleanup() 26 | 27 | 28 | def create_notification( 29 | title: str = "Notification", 30 | subtitle: str | None = None, 31 | text: str | None = None, 32 | icon: str | Path | None = None, 33 | sound: str | None = None, 34 | delay: timedelta = timedelta(), 35 | action_button_str: str | None = None, 36 | action_callback: Callable[[], None] | None = None, 37 | reply_button_str: str | None = None, 38 | reply_callback: Callable[[str], None] | None = None, 39 | snooze_button_str: str | None = None, 40 | ) -> Notification : 41 | """ 42 | Create a MacOS notification :) 43 | :param title: Title of the notification. 44 | :param subtitle: The subtitle of the notification. 45 | :param text: The text/main body of the notification. 46 | :param icon: An Icon you would like to set on the right bottom. 47 | :param delay: Delay before showing the message. 48 | :param action_button_str: The string of the Action button. 49 | :param action_callback: The function to call when the action button is pressed. 50 | :param reply_button_str: The string of the Reply button. 51 | :param reply_callback: The function to call with the replied text. 52 | :param snooze_button_str: This is a useless button that closes the notification (but not the process). Think of 53 | this as a snooze button. 54 | """ 55 | notification_config = NotificationConfig( 56 | title=title, 57 | subtitle=subtitle, 58 | text=text, 59 | icon=(str(icon.resolve()) if isinstance(icon, Path) else icon) if icon else None, 60 | sound=sound, 61 | delay=delay, 62 | action_button_str=action_button_str, 63 | action_callback=action_callback, 64 | reply_button_str=reply_button_str, 65 | reply_callback=reply_callback, 66 | snooze_button_str=snooze_button_str, 67 | ) 68 | return get_notification_manager().create_notification(notification_config) 69 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project information 2 | site_name: macos-notifications 3 | site_url: https://jorricks.github.io/macos-notifications 4 | site_author: Jorrick Sleijster 5 | site_description: Mac Notifications - Simple interactable Mac notifications without any external dependencies. 6 | 7 | # Repository 8 | repo_name: jorricks/macos-notifications 9 | repo_url: https://github.com/jorricks/macos-notifications 10 | 11 | 12 | # Copyright 13 | copyright: Copyright © 2022 Jorrick Sleijster 14 | 15 | # Configuration 16 | theme: 17 | icon: 18 | logo: material/calendar-heart 19 | name: material 20 | palette: 21 | # Palette toggle for light mode 22 | - scheme: default 23 | toggle: 24 | icon: material/brightness-7 25 | name: Switch to dark mode 26 | 27 | # Palette toggle for dark mode 28 | - scheme: slate 29 | toggle: 30 | icon: material/brightness-4 31 | name: Switch to light mode 32 | 33 | # Plugins 34 | plugins: 35 | - search 36 | - termynal 37 | - autorefs 38 | - mkdocstrings: 39 | enable_inventory: true 40 | handlers: 41 | python: 42 | import: 43 | - https://docs.python.org/3/objects.inv 44 | - https://pyobjc.readthedocs.io/en/latest/objects.inv 45 | options: 46 | filters: 47 | - "!__repr__" 48 | - "!__eq__" 49 | annotations_path: brief 50 | show_root_heading: true 51 | show_root_full_path: false 52 | docstring_style: sphinx 53 | line_length: 120 54 | show_signature_annotations: false 55 | show_source: true 56 | docstring_options: 57 | ignore_init_summary: yes 58 | 59 | # Customization 60 | extra: 61 | social: 62 | - icon: fontawesome/brands/github 63 | link: https://github.com/Jorricks/macos-notifications 64 | - icon: fontawesome/brands/python 65 | link: https://pypi.org/user/jorricks/ 66 | - icon: fontawesome/brands/linkedin 67 | link: https://www.linkedin.com/in/jorricks/ 68 | 69 | # Extensions 70 | markdown_extensions: 71 | - admonition 72 | - attr_list 73 | - md_in_html 74 | - pymdownx.emoji: 75 | emoji_index: !!python/name:materialx.emoji.twemoji 76 | emoji_generator: !!python/name:materialx.emoji.to_svg 77 | - pymdownx.highlight 78 | - pymdownx.inlinehilite 79 | - pymdownx.details 80 | - pymdownx.superfences 81 | 82 | # Page tree 83 | nav: 84 | - Home: index.md 85 | - User guide: user_guide.md 86 | - Examples: examples.md 87 | - Code documentation: 88 | - Client: code/client.md 89 | - Config: code/notification_config.md 90 | - Process: code/notification_process.md 91 | - Sender: code/notification_sender.md 92 | - Singleton: code/singleton.md 93 | - Frequently asked questions: faq.md 94 | - Release notes: release-notes.md 95 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "macos-notifications" 7 | authors = [{name = "Jorrick Sleijster", email = "jorricks3@gmail.com"}] 8 | readme = "README.md" 9 | license = {file = "LICENSE"} 10 | classifiers = [ 11 | "Intended Audience :: Information Technology", 12 | "Intended Audience :: Developers", 13 | "Operating System :: OS Independent", 14 | "Environment :: MacOS X", 15 | "Environment :: MacOS X :: Aqua", 16 | "Environment :: MacOS X :: Carbon", 17 | "Environment :: MacOS X :: Cocoa", 18 | "Topic :: Software Development", 19 | "Topic :: Software Development :: Libraries", 20 | "Topic :: Software Development :: Libraries :: Python Modules", 21 | "Topic :: Software Development :: Libraries :: Application Frameworks", 22 | "Typing :: Typed", 23 | "License :: OSI Approved :: MIT License", 24 | "Development Status :: 4 - Beta", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3 :: Only", 28 | "Programming Language :: Python :: 3.7", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | 33 | ] 34 | dynamic = ["version", "description"] 35 | requires-python = ">=3.7" 36 | dependencies = [ 37 | "pyobjc-core>=9.1.1", 38 | "pyobjc-framework-Cocoa>=9.1.1", 39 | ] 40 | 41 | [project.optional-dependencies] 42 | test = [ 43 | "flake8 >=4.0.0,<5.0.0", 44 | "black >= 22.6.0,<23.0.0", 45 | "isort >=5.10.1,<6.0.0", 46 | "mypy ==0.910", 47 | ] 48 | doc = [ 49 | "mkdocs >=1.3.0,<2.0.0", 50 | "mkdocs-material >=8.3.9,<9.0.0", 51 | "mkdocstrings[python] >=0.19.0,<1.0.0", 52 | "termynal >=0.2.0,<1.0.0", 53 | ] 54 | dev = [ 55 | "pre-commit >=2.19.0,<3.0.0", 56 | ] 57 | 58 | 59 | [project.urls] 60 | Home = "https://github.com/Jorricks/macos-notifications" 61 | Documentation = "https://jorricks.github.io/macos-notifications" 62 | Source = "https://github.com/Jorricks/macos-notifications" 63 | PullRequests = "https://github.com/Jorricks/macos-notifications/pulls" 64 | Issues = "https://github.com/Jorricks/macos-notifications/issues" 65 | 66 | [tool.flit.module] 67 | name = "mac_notifications" 68 | 69 | [tool.black] 70 | line-length=120 71 | target-version=['py38'] 72 | 73 | [tool.isort] 74 | line_length = 120 75 | multi_line_output = 3 76 | force_alphabetical_sort_within_sections = "True" 77 | force_sort_within_sections = "False" 78 | known_macnotify = ["mac_notifications"] 79 | sections=["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER", "MACNOTIFY"] 80 | profile = "black" 81 | add_imports = ["from __future__ import annotations"] 82 | 83 | [tool.mypy] 84 | python_version = "3.8" 85 | ignore_missing_imports = "True" 86 | scripts_are_modules = "True" 87 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Mac Notifications 2 |

3 | macos-notifications 4 |

5 |

6 | 7 | Mac supported 8 | 9 | 10 | Package version 11 | 12 | 13 | Supported Python versions 14 | 15 |

16 | 17 | --- 18 | 19 | **Documentation**: [https://jorricks.github.io/macos-notifications/](https://jorricks.github.io/macos-notifications/) 20 | 21 | **Source Code**: [https://github.com/Jorricks/macos-notifications](https://github.com/Jorricks/macos-notifications/) 22 | 23 | --- 24 | 25 | **mac-notification** is a Python library to make it as easy as possible to create interactable notifications. 26 | 27 | 28 | ## Installation 29 | To use macos-notifications, first install it using pip: 30 | 31 | 32 | ``` 33 | $ pip install macos-notifications 34 | ---> 100% 35 | Installed 36 | ``` 37 | 38 | ## Features 39 | - 🚀 Easy python interface. It's as simple as '`client.create_notification(title="Meeting starts now!", subtitle="Team Standup")`' 40 | - 💥 Ability to add action buttons with callbacks! 41 | - 📝 Ability to reply to notifications! 42 | - ⌚ Delayed notifications. 43 | - ⏱️ Automatically time out the notification listener. 44 | - 📦 Just `pyobjc` as a dependency. 45 | 46 | 47 | ## Example 48 | 49 | ```python 50 | from functools import partial 51 | from mac_notifications import client 52 | 53 | if __name__ == "__main__": 54 | client.create_notification( 55 | title="Meeting starts now!", 56 | subtitle="Team Standup", 57 | icon="/Users/jorrick/zoom.png", 58 | sound="Frog", 59 | action_button_str="Join zoom meeting", 60 | action_callback=partial(join_zoom_meeting, conf_number=zoom_conf_number) 61 | ) 62 | ``` 63 | A simple example. Please look [in the docs](https://jorricks.github.io/macos-notifications/) for more examples like this: 64 | 65 |

66 | 67 | macos-notifications 68 | 69 |

70 | 71 | ## Limitations 72 | - You need to keep your application running while waiting for the callback to happen. 73 | - Currently, we are only supporting the old deprecated [user notifications](https://developer.apple.com/documentation/foundation/nsusernotification). Soon we will also make the new implementation available. 74 | -------------------------------------------------------------------------------- /src/mac_notifications/notification_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from dataclasses import dataclass, field 5 | from datetime import timedelta 6 | from typing import Callable 7 | 8 | """ 9 | The dataclasses that represent a Notification configuration. 10 | """ 11 | 12 | 13 | @dataclass 14 | class NotificationConfig: 15 | """ 16 | The standard representation of a Notifications. This is used inside the main process. 17 | """ 18 | 19 | title: str 20 | subtitle: str | None 21 | text: str | None 22 | icon: str | None 23 | delay: timedelta 24 | action_button_str: str | None 25 | action_callback: Callable[[], None] | None 26 | reply_button_str: str | None 27 | reply_callback: Callable[[str], None] | None 28 | snooze_button_str: str | None 29 | sound: str | None 30 | uid: str = field(default_factory=lambda: uuid.uuid4().hex) 31 | 32 | @property 33 | def contains_callback(self) -> bool: 34 | return bool(self.action_callback or self.reply_callback) 35 | 36 | @staticmethod 37 | def c_compliant(a_str: str | None) -> str | None: 38 | return "".join(filter(lambda x: bool(str.isalnum or str.isspace), a_str)) if a_str else None # type: ignore 39 | 40 | def to_json_notification(self) -> "JSONNotificationConfig": 41 | return JSONNotificationConfig( 42 | title=NotificationConfig.c_compliant(self.title) or "Notification title", 43 | subtitle=NotificationConfig.c_compliant(self.subtitle), 44 | text=NotificationConfig.c_compliant(self.text), 45 | icon=self.icon, 46 | sound=self.sound, 47 | delay_in_seconds=(self.delay or timedelta()).total_seconds(), 48 | action_button_str=NotificationConfig.c_compliant(self.action_button_str), 49 | action_callback_present=bool(self.action_callback), 50 | reply_button_str=NotificationConfig.c_compliant(self.reply_button_str), 51 | reply_callback_present=bool(self.reply_callback), 52 | snooze_button_str=NotificationConfig.c_compliant(self.snooze_button_str), 53 | uid=self.uid, 54 | ) 55 | 56 | 57 | @dataclass 58 | class JSONNotificationConfig: 59 | """ 60 | This notification configuration class that only contains serializable parts. 61 | 62 | This class is required because waiting for user interaction with a notification is a blocking operation. 63 | Because it is a blocking operation, if we want to be able to receive any user interaction from the notification, 64 | without completely halting/freezing our main process, we need to open it in a background process. However, to be 65 | able to transfer the data from the notification to the other process, all the arguments should be serializable. As 66 | callbacks/functions are not serializable, we replaced them by booleans on whether it contained a callback or not. 67 | Once a callback should be triggered, we send a message over a multiprocessing Queue and trigger the callback in 68 | the main process. 69 | """ 70 | 71 | title: str 72 | subtitle: str | None 73 | text: str | None 74 | icon: str | None 75 | sound: str | None 76 | delay_in_seconds: float 77 | action_button_str: str | None 78 | action_callback_present: bool 79 | reply_button_str: str | None 80 | reply_callback_present: bool 81 | snooze_button_str: str | None 82 | uid: str 83 | 84 | @property 85 | def contains_callback(self) -> bool: 86 | return bool(self.action_callback_present or self.reply_callback_present) 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | macos-notifications 3 |

4 |

5 | 6 | Mac supported 7 | 8 | 9 | Package version 10 | 11 | 12 | Supported Python versions 13 | 14 |

15 | 16 | --- 17 | 18 | **Documentation**: [https://jorricks.github.io/macos-notifications/](https://jorricks.github.io/macos-notifications/) 19 | 20 | **Source Code**: [https://github.com/Jorricks/macos-notifications](https://github.com/Jorricks/macos-notifications) 21 | 22 | --- 23 | 24 | **mac-notification** is a Python library to make it as easy as possible to create interactable notifications. 25 | 26 | 27 | ## Installation 28 | 29 | To use macos-notifications, first install it using pip: 30 | 31 | pip install macos-notifications 32 | 33 | 34 | ## Features 35 | - 🚀 Easy python interface. It's as simple as '`client.create_notification(title="Meeting starts now!", subtitle="Team Standup")`' 36 | - 💥 Ability to add action buttons with callbacks! 37 | - 📝 Ability to reply to notifications! 38 | - ⌚ Delayed notifications. 39 | - ⏱️ Automatically time out the notification listener. 40 | - 📦 Just `pyobjc` as a dependency. 41 | 42 | ## Example 43 | ```python 44 | from functools import partial 45 | from mac_notifications import client 46 | 47 | if __name__ == "__main__": 48 | client.create_notification( 49 | title="Meeting starts now!", 50 | subtitle="Team Standup", 51 | icon="/Users/jorrick/zoom.png", 52 | sound="Frog", 53 | action_button_str="Join zoom meeting", 54 | action_callback=partial(join_zoom_meeting, conf_number=zoom_conf_number) 55 | ) 56 | ``` 57 | A simple example. Please look [in the docs](https://jorricks.github.io/macos-notifications/) for more examples like this: 58 | 59 |

60 | 61 | macos-notifications 62 | 63 |

64 | 65 | ## Why did you create this library? 66 | I wanted a library that did not depend on any non-python tools (so you had to go around and install that). Instead, I wanted a library where you install the pip packages, and you are done. 67 | Later I realised how hard it was to integrate correctly with PyOBJC. Also, I had a hard time finding any examples on how to easily integrate this in a non-blocking fashion with my tool. 68 | Hence, I figured I should set it up to be as user-friendly as possible and share it with the world ;)! 69 | 70 | 71 | ## Limitations 72 | Although there are some limitations, there is no reason to not use it now :v:. 73 | - You need to keep your application running while waiting for the callback to happen. 74 | - We do not support raising notifications from anything but the main thread. If you wish to raise it from other threads, you need to set up a communication channel with the main thread, which in turn than raises the notification. 75 | - Currently, we are only supporting the old deprecated [user notifications](https://developer.apple.com/documentation/foundation/nsusernotification). If you wish to use the new implementation, please feel free to propose an MR. 76 | - You can not change the main image of the notification to be project specific. You can only change the Python interpreter image, but that would impact all notifications send by Python. 77 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | On this page we will list several examples. Let's start with a demonstration of the last example of this page. 4 | 5 |

6 | macos-notifications 7 |

8 | 9 | ## Simple notification 10 | ```python 11 | from mac_notifications import client 12 | 13 | 14 | if __name__ == "__main__": 15 | client.create_notification( 16 | title="Meeting starts now!", 17 | subtitle="Team Standup" 18 | ) 19 | ``` 20 | 21 | 22 | ## Notification with a reply callback 23 | ```python 24 | from __future__ import annotations 25 | 26 | import time 27 | from datetime import timedelta 28 | from pathlib import Path 29 | 30 | from mac_notifications import client 31 | 32 | 33 | if __name__ == "__main__": 34 | print("You have to press the notification within 30 seconds for it to work.") 35 | client.create_notification( 36 | title="Cool notification", 37 | subtitle="Subtitle of the notification", 38 | text="Hello, I contain info", 39 | icon=Path(__file__).parent / "zoom.png", 40 | delay=timedelta(milliseconds=500), 41 | reply_button_str="Reply to this notification", 42 | reply_callback=lambda reply: print(f"Replied {reply=}"), 43 | snooze_button_str="Or click me", 44 | ) 45 | time.sleep(30) 46 | client.stop_listening_for_callbacks() 47 | ``` 48 | 49 | ## Notification with an action 50 | ```python 51 | from __future__ import annotations 52 | 53 | import time 54 | from functools import partial 55 | from pathlib import Path 56 | 57 | from mac_notifications import client 58 | 59 | 60 | def join_zoom_meeting(conf_number: int | str) -> None: 61 | """Join the zoom meeting""" 62 | # import subprocess 63 | # subprocess.run(f'open "zoommtg://zoom.us/join?action=join&confno={conf_number}&browser=chrome"', shell=True) 64 | print(f"Opened zoom into meeting with {conf_number=}.") 65 | 66 | 67 | if __name__ == "__main__": 68 | print(client.get_notification_manager().get_active_running_notifications()) 69 | client.create_notification( 70 | title="Meeting starts now!", 71 | subtitle="Standup time :)", 72 | icon=Path(__file__).parent / "zoom.png", 73 | action_button_str="Join zoom meeting", 74 | action_callback=partial(join_zoom_meeting, conf_number="12345678"), 75 | ) 76 | time.sleep(30) 77 | client.stop_listening_for_callbacks() 78 | ``` 79 | 80 | 81 | ## Multiple notifications 82 | Give this a try. Play around with the notifications. 83 | Notice that when you close the notification, the count doesn't go down and the application stays running forever. 84 | This is one of the limitations of using Python for these notifications as we don't know whether the notification is 85 | still present or not. 86 | ```python 87 | import time 88 | from mac_notifications import client 89 | 90 | 91 | if __name__ == "__main__": 92 | print(f"Active number of notifications: {client.get_notification_manager().get_active_running_notifications()}") 93 | client.create_notification( 94 | title="Action notification", 95 | subtitle="Subtitle of the notification", 96 | action_button_str="Perform an action", 97 | action_callback=lambda: print("Pressed action button"), 98 | ) 99 | 100 | time.sleep(1) 101 | client.create_notification( 102 | title="Reply notification", 103 | subtitle="Subtitle of the notification", 104 | reply_button_str="Reply to this notification", 105 | reply_callback=lambda reply: print(f"Replied {reply=}"), 106 | ) 107 | 108 | while client.get_notification_manager().get_active_running_notifications() > 0: 109 | time.sleep(1) 110 | print(f"Active number of notifications: {client.get_notification_manager().get_active_running_notifications()}") 111 | client.stop_listening_for_callbacks() 112 | ``` -------------------------------------------------------------------------------- /src/mac_notifications/notification_sender.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from multiprocessing import SimpleQueue 5 | from typing import Any 6 | 7 | from AppKit import NSImage 8 | from Foundation import NSDate, NSObject, NSURL, NSUserNotification, NSUserNotificationCenter 9 | from PyObjCTools import AppHelper 10 | 11 | from mac_notifications.notification_config import JSONNotificationConfig 12 | 13 | logger = logging.getLogger() 14 | 15 | """ 16 | This module is responsible for creating the notifications in the C-layer and listening/reporting about user activity. 17 | """ 18 | 19 | def create_notification(config: JSONNotificationConfig, queue_to_submit_events_to: SimpleQueue | None) -> Any: 20 | """ 21 | Create a notification and possibly listed & report about notification activity. 22 | :param config: The configuration of the notification to send. 23 | :param queue_to_submit_events_to: The Queue to submit user activity related to the callbacks to. If this argument 24 | is passed, it will start the event listener after it created the Notifications. If this is None, it will only 25 | create the notification. 26 | """ 27 | 28 | class MacOSNotification(NSObject): 29 | def send(self): 30 | """Sending of the notification""" 31 | notification = NSUserNotification.alloc().init() 32 | notification.setIdentifier_(config.uid) 33 | if config is not None: 34 | notification.setTitle_(config.title) 35 | if config.subtitle is not None: 36 | notification.setSubtitle_(config.subtitle) 37 | if config.text is not None: 38 | notification.setInformativeText_(config.text) 39 | if config.sound is not None: 40 | notification.setSoundName_(config.sound) 41 | if config.icon is not None: 42 | url = NSURL.alloc().initWithString_(f"file://{config.icon}") 43 | image = NSImage.alloc().initWithContentsOfURL_(url) 44 | notification.setContentImage_(image) 45 | 46 | # Notification buttons (main action button and other button) 47 | if config.action_button_str: 48 | notification.setActionButtonTitle_(config.action_button_str) 49 | notification.setHasActionButton_(True) 50 | 51 | if config.snooze_button_str: 52 | notification.setOtherButtonTitle_(config.snooze_button_str) 53 | 54 | if config.reply_callback_present: 55 | notification.setHasReplyButton_(True) 56 | if config.reply_button_str: 57 | notification.setResponsePlaceholder_(config.reply_button_str) 58 | 59 | NSUserNotificationCenter.defaultUserNotificationCenter().setDelegate_(self) 60 | 61 | # Setting delivery date as current date + delay (in seconds) 62 | notification.setDeliveryDate_( 63 | NSDate.dateWithTimeInterval_sinceDate_(config.delay_in_seconds, NSDate.date()) 64 | ) 65 | 66 | # Schedule the notification send 67 | NSUserNotificationCenter.defaultUserNotificationCenter().scheduleNotification_(notification) 68 | 69 | # Wait for the notification CallBack to happen. 70 | if queue_to_submit_events_to: 71 | logger.debug("Started listening for user interactions with notifications.") 72 | AppHelper.runConsoleEventLoop() 73 | 74 | def userNotificationCenter_didDeliverNotification_( 75 | self, center: "_NSConcreteUserNotificationCenter", notif: "_NSConcreteUserNotification" # type: ignore # noqa 76 | ) -> None: 77 | """Respond to the delivering of the notification.""" 78 | logger.debug(f"Delivered: {notif.identifier()}") 79 | 80 | def userNotificationCenter_didActivateNotification_( 81 | self, center: "_NSConcreteUserNotificationCenter", notif: "_NSConcreteUserNotification" # type: ignore # noqa 82 | ) -> None: 83 | """ 84 | Respond to a user interaction with the notification. 85 | """ 86 | identifier = notif.identifier() 87 | response = notif.response() 88 | activation_type = notif.activationType() 89 | 90 | if queue_to_submit_events_to is None: 91 | raise ValueError("Queue should not be None here.") 92 | else: 93 | queue: SimpleQueue = queue_to_submit_events_to 94 | 95 | logger.debug(f"User interacted with {identifier} with activationType {activation_type}.") 96 | if activation_type == 1: 97 | # user clicked on the notification (not on a button) 98 | pass 99 | 100 | elif activation_type == 2: # user clicked on the action button 101 | queue.put((identifier, "action_button_clicked", "")) 102 | 103 | elif activation_type == 3: # User clicked on the reply button 104 | queue.put((identifier, "reply_button_clicked", response.string())) 105 | 106 | # create the new notification 107 | new_notif = MacOSNotification.alloc().init() 108 | 109 | # return notification 110 | return new_notif 111 | 112 | 113 | def cancel_notification(uid:str) -> None: 114 | notification = NSUserNotification.alloc().init() 115 | notification.setIdentifier_(uid) 116 | NSUserNotificationCenter.defaultUserNotificationCenter().removeDeliveredNotification_(notification) 117 | -------------------------------------------------------------------------------- /src/mac_notifications/manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import atexit 4 | import logging 5 | import signal 6 | import sys 7 | import time 8 | from multiprocessing import SimpleQueue 9 | from threading import Event, Thread 10 | from typing import Dict, List 11 | 12 | from mac_notifications.listener_process import NotificationProcess 13 | from mac_notifications.notification_config import NotificationConfig 14 | from mac_notifications.singleton import Singleton 15 | from mac_notifications.notification_sender import cancel_notification 16 | 17 | """ 18 | This is the module responsible for managing the notifications over time & enabling callbacks to be executed. 19 | """ 20 | 21 | # Once we have created more than _MAX_NUMBER_OF_CALLBACKS_TO_TRACK notifications with a callback, we remove the older 22 | # callbacks. 23 | _MAX_NUMBER_OF_CALLBACKS_TO_TRACK: int = 1000 24 | # The _FIFO_LIST keeps track of the order of notifications with callbacks. This way we know what to remove after having 25 | # more than _MAX_NUMBER_OF_CALLBACKS_TO_TRACK number of notifications with a callback. 26 | _FIFO_LIST: List[str] = [] 27 | # The _NOTIFICATION_MAP is required to keep track of notifications that have a callback. We map the UID of the 28 | # notification to the process that was started for it and the configuration. 29 | # Note: _NOTIFICATION_MAP should only contain notifications with a callback! 30 | _NOTIFICATION_MAP: Dict[str, NotificationConfig] = {} 31 | logger = logging.getLogger() 32 | 33 | 34 | class Notification(object): 35 | def __init__(self, uid) -> None: 36 | self.uid = uid 37 | 38 | def cancel(self) -> None: 39 | cancel_notification(self.uid) 40 | clear_notification_from_existence(self.uid) 41 | 42 | 43 | class NotificationManager(metaclass=Singleton): 44 | """ 45 | The NotificationManager is responsible for managing the notifications. This includes the following: 46 | - Starting new notifications. 47 | - Starting the Callback Executor thread in the background. 48 | """ 49 | 50 | def __init__(self): 51 | self._callback_queue: SimpleQueue = SimpleQueue() 52 | self._callback_executor_event: Event = Event() 53 | self._callback_executor_thread: CallbackExecutorThread | None = None 54 | self._callback_listener_process: NotificationProcess | None = None 55 | # Specify that once we stop our application, self.cleanup should run 56 | atexit.register(self.cleanup) 57 | # Specify that when we get a keyboard interrupt, this function should handle it 58 | signal.signal(signal.SIGINT, handler=self.catch_keyboard_interrupt) 59 | 60 | def create_callback_executor_thread(self) -> None: 61 | """Creates the callback executor thread and sets the _callback_executor_event.""" 62 | if not (self._callback_executor_thread and self._callback_executor_thread.is_alive()): 63 | self._callback_executor_thread = CallbackExecutorThread( 64 | keep_running=self._callback_executor_event, 65 | callback_queue=self._callback_queue, 66 | ) 67 | self._callback_executor_event.set() 68 | self._callback_executor_thread.start() 69 | 70 | def create_notification(self, notification_config: NotificationConfig) -> Notification: 71 | """ 72 | Create a notification and the corresponding processes if required for a notification with callbacks. 73 | :param notification_config: The configuration for the notification. 74 | """ 75 | json_config = notification_config.to_json_notification() 76 | if not notification_config.contains_callback or self._callback_listener_process is not None: 77 | # We can send it directly and kill the process after as we don't need to listen for callbacks. 78 | new_process = NotificationProcess(json_config, None) 79 | new_process.start() 80 | new_process.join(timeout=5) 81 | else: 82 | # We need to also start a listener, so we send the json through a separate process. 83 | self._callback_listener_process = NotificationProcess(json_config, self._callback_queue) 84 | self._callback_listener_process.start() 85 | self.create_callback_executor_thread() 86 | 87 | if notification_config.contains_callback: 88 | _FIFO_LIST.append(notification_config.uid) 89 | _NOTIFICATION_MAP[notification_config.uid] = notification_config 90 | self.clear_old_notifications() 91 | return Notification(notification_config.uid) 92 | 93 | @staticmethod 94 | def clear_old_notifications() -> None: 95 | """Removes old notifications when we are passed our threshold.""" 96 | while len(_FIFO_LIST) > _MAX_NUMBER_OF_CALLBACKS_TO_TRACK: 97 | clear_notification_from_existence(_FIFO_LIST.pop(0)) 98 | 99 | @staticmethod 100 | def get_active_running_notifications() -> int: 101 | """ 102 | WARNING! This is wildly inaccurate. 103 | Does an attempt to get the number of active running notifications. However, if a user snoozed or deleted the 104 | notification, we don't get an update. 105 | """ 106 | return len(_NOTIFICATION_MAP) 107 | 108 | def catch_keyboard_interrupt(self, *args) -> None: 109 | """We catch the keyboard interrupt but also pass it onto the user program.""" 110 | self.cleanup() 111 | sys.exit(signal.SIGINT) 112 | 113 | def cleanup(self) -> None: 114 | """Stop all processes related to the Notification callback handling.""" 115 | if self._callback_executor_thread: 116 | self._callback_executor_event.clear() 117 | self._callback_executor_thread.join() 118 | if self._callback_listener_process: 119 | self._callback_listener_process.kill() 120 | self._callback_executor_thread = None 121 | self._callback_listener_process = None 122 | _NOTIFICATION_MAP.clear() 123 | _FIFO_LIST.clear() 124 | 125 | 126 | class CallbackExecutorThread(Thread): 127 | """ 128 | Background threat that checks each 0.1 second whether there are any callbacks that it should execute. 129 | """ 130 | 131 | def __init__(self, keep_running: Event, callback_queue: SimpleQueue): 132 | super().__init__() 133 | self.event_indicating_to_continue = keep_running 134 | self.callback_queue = callback_queue 135 | 136 | def run(self) -> None: 137 | while self.event_indicating_to_continue.is_set(): 138 | self.drain_queue() 139 | time.sleep(0.1) 140 | 141 | def drain_queue(self) -> None: 142 | """ 143 | This drains the Callback Queue. When there is a notification for which a callback should be fired, this event is 144 | added to the `callback_queue`. This background Threat is then responsible for listening in on the callback_queue 145 | and when there is a callback it should execute, it executes it. 146 | """ 147 | while not self.callback_queue.empty(): 148 | msg = self.callback_queue.get() 149 | notification_uid, event_id, reply_text = msg 150 | if notification_uid not in _NOTIFICATION_MAP: 151 | logger.debug(f"Received a notification interaction for {notification_uid} which we don't know.") 152 | continue 153 | 154 | if event_id == "action_button_clicked": 155 | notification_config = _NOTIFICATION_MAP.pop(notification_uid) 156 | logger.debug(f"Executing reply callback for notification {notification_config.title}.") 157 | if notification_config.action_callback is None: 158 | raise ValueError(f"Notifications action button pressed without callback: {notification_config}.") 159 | else: 160 | notification_config.action_callback() 161 | elif event_id == "reply_button_clicked": 162 | notification_config = _NOTIFICATION_MAP.pop(notification_uid) 163 | logger.debug(f"Executing reply callback for notification {notification_config.title}, {reply_text}.") 164 | if notification_config.reply_callback is None: 165 | raise ValueError(f"Notifications reply button pressed without callback: {notification_config}.") 166 | else: 167 | notification_config.reply_callback(reply_text) 168 | else: 169 | raise ValueError(f"Unknown event_id: {event_id}.") 170 | clear_notification_from_existence(notification_uid) 171 | 172 | 173 | def clear_notification_from_existence(notification_id: str) -> None: 174 | """Removes all records we had of a notification""" 175 | if notification_id in _NOTIFICATION_MAP: 176 | _NOTIFICATION_MAP.pop(notification_id) 177 | if notification_id in _FIFO_LIST: 178 | _FIFO_LIST.remove(notification_id) 179 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,linux,windows,pycharm+all,visualstudio,intellij+all,python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,linux,windows,pycharm+all,visualstudio,intellij+all,python 3 | 4 | ### Intellij+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # AWS User-specific 16 | .idea/**/aws.xml 17 | 18 | # Generated files 19 | .idea/**/contentModel.xml 20 | 21 | # Sensitive or high-churn files 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.local.xml 25 | .idea/**/sqlDataSources.xml 26 | .idea/**/dynamic.xml 27 | .idea/**/uiDesigner.xml 28 | .idea/**/dbnavigator.xml 29 | 30 | # Gradle 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Gradle and Maven with auto-import 35 | # When using Gradle or Maven with auto-import, you should exclude module files, 36 | # since they will be recreated, and may cause churn. Uncomment if using 37 | # auto-import. 38 | # .idea/artifacts 39 | # .idea/compiler.xml 40 | # .idea/jarRepositories.xml 41 | # .idea/modules.xml 42 | # .idea/*.iml 43 | # .idea/modules 44 | # *.iml 45 | # *.ipr 46 | 47 | # CMake 48 | cmake-build-*/ 49 | 50 | # Mongo Explorer plugin 51 | .idea/**/mongoSettings.xml 52 | 53 | # File-based project format 54 | *.iws 55 | 56 | # IntelliJ 57 | out/ 58 | 59 | # mpeltonen/sbt-idea plugin 60 | .idea_modules/ 61 | 62 | # JIRA plugin 63 | atlassian-ide-plugin.xml 64 | 65 | # Cursive Clojure plugin 66 | .idea/replstate.xml 67 | 68 | # SonarLint plugin 69 | .idea/sonarlint/ 70 | 71 | # Crashlytics plugin (for Android Studio and IntelliJ) 72 | com_crashlytics_export_strings.xml 73 | crashlytics.properties 74 | crashlytics-build.properties 75 | fabric.properties 76 | 77 | # Editor-based Rest Client 78 | .idea/httpRequests 79 | 80 | # Android studio 3.1+ serialized cache file 81 | .idea/caches/build_file_checksums.ser 82 | 83 | ### Intellij+all Patch ### 84 | # Ignore everything but code style settings and run configurations 85 | # that are supposed to be shared within teams. 86 | 87 | .idea/* 88 | 89 | !.idea/codeStyles 90 | !.idea/runConfigurations 91 | 92 | ### Linux ### 93 | *~ 94 | 95 | # temporary files which can be created if a process still has a handle open of a deleted file 96 | .fuse_hidden* 97 | 98 | # KDE directory preferences 99 | .directory 100 | 101 | # Linux trash folder which might appear on any partition or disk 102 | .Trash-* 103 | 104 | # .nfs files are created when an open file is removed but is still being accessed 105 | .nfs* 106 | 107 | ### macOS ### 108 | # General 109 | .DS_Store 110 | .AppleDouble 111 | .LSOverride 112 | 113 | # Icon must end with two \r 114 | Icon 115 | 116 | 117 | # Thumbnails 118 | ._* 119 | 120 | # Files that might appear in the root of a volume 121 | .DocumentRevisions-V100 122 | .fseventsd 123 | .Spotlight-V100 124 | .TemporaryItems 125 | .Trashes 126 | .VolumeIcon.icns 127 | .com.apple.timemachine.donotpresent 128 | 129 | # Directories potentially created on remote AFP share 130 | .AppleDB 131 | .AppleDesktop 132 | Network Trash Folder 133 | Temporary Items 134 | .apdisk 135 | 136 | ### PyCharm+all ### 137 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 138 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 139 | 140 | # User-specific stuff 141 | 142 | # AWS User-specific 143 | 144 | # Generated files 145 | 146 | # Sensitive or high-churn files 147 | 148 | # Gradle 149 | 150 | # Gradle and Maven with auto-import 151 | # When using Gradle or Maven with auto-import, you should exclude module files, 152 | # since they will be recreated, and may cause churn. Uncomment if using 153 | # auto-import. 154 | # .idea/artifacts 155 | # .idea/compiler.xml 156 | # .idea/jarRepositories.xml 157 | # .idea/modules.xml 158 | # .idea/*.iml 159 | # .idea/modules 160 | # *.iml 161 | # *.ipr 162 | 163 | # CMake 164 | 165 | # Mongo Explorer plugin 166 | 167 | # File-based project format 168 | 169 | # IntelliJ 170 | 171 | # mpeltonen/sbt-idea plugin 172 | 173 | # JIRA plugin 174 | 175 | # Cursive Clojure plugin 176 | 177 | # SonarLint plugin 178 | 179 | # Crashlytics plugin (for Android Studio and IntelliJ) 180 | 181 | # Editor-based Rest Client 182 | 183 | # Android studio 3.1+ serialized cache file 184 | 185 | ### PyCharm+all Patch ### 186 | # Ignore everything but code style settings and run configurations 187 | # that are supposed to be shared within teams. 188 | 189 | 190 | 191 | ### Python ### 192 | # Byte-compiled / optimized / DLL files 193 | __pycache__/ 194 | *.py[cod] 195 | *$py.class 196 | 197 | # C extensions 198 | *.so 199 | 200 | # Distribution / packaging 201 | .Python 202 | build/ 203 | develop-eggs/ 204 | dist/ 205 | downloads/ 206 | eggs/ 207 | .eggs/ 208 | lib/ 209 | lib64/ 210 | parts/ 211 | sdist/ 212 | var/ 213 | wheels/ 214 | share/python-wheels/ 215 | *.egg-info/ 216 | .installed.cfg 217 | *.egg 218 | MANIFEST 219 | 220 | # PyInstaller 221 | # Usually these files are written by a python script from a template 222 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 223 | *.manifest 224 | *.spec 225 | 226 | # Installer logs 227 | pip-log.txt 228 | pip-delete-this-directory.txt 229 | 230 | # Unit test / coverage reports 231 | htmlcov/ 232 | .tox/ 233 | .nox/ 234 | .coverage 235 | .coverage.* 236 | .cache 237 | nosetests.xml 238 | coverage.xml 239 | *.cover 240 | *.py,cover 241 | .hypothesis/ 242 | .pytest_cache/ 243 | cover/ 244 | 245 | # Translations 246 | *.mo 247 | *.pot 248 | 249 | # Django stuff: 250 | *.log 251 | local_settings.py 252 | db.sqlite3 253 | db.sqlite3-journal 254 | 255 | # Flask stuff: 256 | instance/ 257 | .webassets-cache 258 | 259 | # Scrapy stuff: 260 | .scrapy 261 | 262 | # Sphinx documentation 263 | docs/_build/ 264 | 265 | # PyBuilder 266 | .pybuilder/ 267 | target/ 268 | 269 | # Jupyter Notebook 270 | .ipynb_checkpoints 271 | 272 | # IPython 273 | profile_default/ 274 | ipython_config.py 275 | 276 | # pyenv 277 | # For a library or package, you might want to ignore these files since the code is 278 | # intended to run in multiple environments; otherwise, check them in: 279 | # .python-version 280 | 281 | # pipenv 282 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 283 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 284 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 285 | # install all needed dependencies. 286 | #Pipfile.lock 287 | 288 | # poetry 289 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 290 | # This is especially recommended for binary packages to ensure reproducibility, and is more 291 | # commonly ignored for libraries. 292 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 293 | #poetry.lock 294 | 295 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 296 | __pypackages__/ 297 | 298 | # Celery stuff 299 | celerybeat-schedule 300 | celerybeat.pid 301 | 302 | # SageMath parsed files 303 | *.sage.py 304 | 305 | # Environments 306 | .env 307 | .venv 308 | env/ 309 | venv/ 310 | ENV/ 311 | env.bak/ 312 | venv.bak/ 313 | 314 | # Spyder project settings 315 | .spyderproject 316 | .spyproject 317 | 318 | # Rope project settings 319 | .ropeproject 320 | 321 | # mkdocs documentation 322 | /site 323 | 324 | # mypy 325 | .mypy_cache/ 326 | .dmypy.json 327 | dmypy.json 328 | 329 | # Pyre type checker 330 | .pyre/ 331 | 332 | # pytype static type analyzer 333 | .pytype/ 334 | 335 | # Cython debug symbols 336 | cython_debug/ 337 | 338 | # PyCharm 339 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 340 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 341 | # and can be added to the global gitignore or merged into this file. For a more nuclear 342 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 343 | #.idea/ 344 | 345 | ### Windows ### 346 | # Windows thumbnail cache files 347 | Thumbs.db 348 | Thumbs.db:encryptable 349 | ehthumbs.db 350 | ehthumbs_vista.db 351 | 352 | # Dump file 353 | *.stackdump 354 | 355 | # Folder config file 356 | [Dd]esktop.ini 357 | 358 | # Recycle Bin used on file shares 359 | $RECYCLE.BIN/ 360 | 361 | # Windows Installer files 362 | *.cab 363 | *.msi 364 | *.msix 365 | *.msm 366 | *.msp 367 | 368 | # Windows shortcuts 369 | *.lnk 370 | 371 | ### VisualStudio ### 372 | ## Ignore Visual Studio temporary files, build results, and 373 | ## files generated by popular Visual Studio add-ons. 374 | ## 375 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 376 | 377 | # User-specific files 378 | *.rsuser 379 | *.suo 380 | *.user 381 | *.userosscache 382 | *.sln.docstates 383 | 384 | # User-specific files (MonoDevelop/Xamarin Studio) 385 | *.userprefs 386 | 387 | # Mono auto generated files 388 | mono_crash.* 389 | 390 | # Build results 391 | [Dd]ebug/ 392 | [Dd]ebugPublic/ 393 | [Rr]elease/ 394 | [Rr]eleases/ 395 | x64/ 396 | x86/ 397 | [Ww][Ii][Nn]32/ 398 | [Aa][Rr][Mm]/ 399 | [Aa][Rr][Mm]64/ 400 | bld/ 401 | [Bb]in/ 402 | [Oo]bj/ 403 | [Ll]og/ 404 | [Ll]ogs/ 405 | 406 | # Visual Studio 2015/2017 cache/options directory 407 | .vs/ 408 | # Uncomment if you have tasks that create the project's static files in wwwroot 409 | #wwwroot/ 410 | 411 | # Visual Studio 2017 auto generated files 412 | Generated\ Files/ 413 | 414 | # MSTest test Results 415 | [Tt]est[Rr]esult*/ 416 | [Bb]uild[Ll]og.* 417 | 418 | # NUnit 419 | *.VisualState.xml 420 | TestResult.xml 421 | nunit-*.xml 422 | 423 | # Build Results of an ATL Project 424 | [Dd]ebugPS/ 425 | [Rr]eleasePS/ 426 | dlldata.c 427 | 428 | # Benchmark Results 429 | BenchmarkDotNet.Artifacts/ 430 | 431 | # .NET Core 432 | project.lock.json 433 | project.fragment.lock.json 434 | artifacts/ 435 | 436 | # ASP.NET Scaffolding 437 | ScaffoldingReadMe.txt 438 | 439 | # StyleCop 440 | StyleCopReport.xml 441 | 442 | # Files built by Visual Studio 443 | *_i.c 444 | *_p.c 445 | *_h.h 446 | *.ilk 447 | *.meta 448 | *.obj 449 | *.iobj 450 | *.pch 451 | *.pdb 452 | *.ipdb 453 | *.pgc 454 | *.pgd 455 | *.rsp 456 | *.sbr 457 | *.tlb 458 | *.tli 459 | *.tlh 460 | *.tmp 461 | *.tmp_proj 462 | *_wpftmp.csproj 463 | *.tlog 464 | *.vspscc 465 | *.vssscc 466 | .builds 467 | *.pidb 468 | *.svclog 469 | *.scc 470 | 471 | # Chutzpah Test files 472 | _Chutzpah* 473 | 474 | # Visual C++ cache files 475 | ipch/ 476 | *.aps 477 | *.ncb 478 | *.opendb 479 | *.opensdf 480 | *.sdf 481 | *.cachefile 482 | *.VC.db 483 | *.VC.VC.opendb 484 | 485 | # Visual Studio profiler 486 | *.psess 487 | *.vsp 488 | *.vspx 489 | *.sap 490 | 491 | # Visual Studio Trace Files 492 | *.e2e 493 | 494 | # TFS 2012 Local Workspace 495 | $tf/ 496 | 497 | # Guidance Automation Toolkit 498 | *.gpState 499 | 500 | # ReSharper is a .NET coding add-in 501 | _ReSharper*/ 502 | *.[Rr]e[Ss]harper 503 | *.DotSettings.user 504 | 505 | # TeamCity is a build add-in 506 | _TeamCity* 507 | 508 | # DotCover is a Code Coverage Tool 509 | *.dotCover 510 | 511 | # AxoCover is a Code Coverage Tool 512 | .axoCover/* 513 | !.axoCover/settings.json 514 | 515 | # Coverlet is a free, cross platform Code Coverage Tool 516 | coverage*.json 517 | coverage*.xml 518 | coverage*.info 519 | 520 | # Visual Studio code coverage results 521 | *.coverage 522 | *.coveragexml 523 | 524 | # NCrunch 525 | _NCrunch_* 526 | .*crunch*.local.xml 527 | nCrunchTemp_* 528 | 529 | # MightyMoose 530 | *.mm.* 531 | AutoTest.Net/ 532 | 533 | # Web workbench (sass) 534 | .sass-cache/ 535 | 536 | # Installshield output folder 537 | [Ee]xpress/ 538 | 539 | # DocProject is a documentation generator add-in 540 | DocProject/buildhelp/ 541 | DocProject/Help/*.HxT 542 | DocProject/Help/*.HxC 543 | DocProject/Help/*.hhc 544 | DocProject/Help/*.hhk 545 | DocProject/Help/*.hhp 546 | DocProject/Help/Html2 547 | DocProject/Help/html 548 | 549 | # Click-Once directory 550 | publish/ 551 | 552 | # Publish Web Output 553 | *.[Pp]ublish.xml 554 | *.azurePubxml 555 | # Note: Comment the next line if you want to checkin your web deploy settings, 556 | # but database connection strings (with potential passwords) will be unencrypted 557 | *.pubxml 558 | *.publishproj 559 | 560 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 561 | # checkin your Azure Web App publish settings, but sensitive information contained 562 | # in these scripts will be unencrypted 563 | PublishScripts/ 564 | 565 | # NuGet Packages 566 | *.nupkg 567 | # NuGet Symbol Packages 568 | *.snupkg 569 | # The packages folder can be ignored because of Package Restore 570 | **/[Pp]ackages/* 571 | # except build/, which is used as an MSBuild target. 572 | !**/[Pp]ackages/build/ 573 | # Uncomment if necessary however generally it will be regenerated when needed 574 | #!**/[Pp]ackages/repositories.config 575 | # NuGet v3's project.json files produces more ignorable files 576 | *.nuget.props 577 | *.nuget.targets 578 | 579 | # Microsoft Azure Build Output 580 | csx/ 581 | *.build.csdef 582 | 583 | # Microsoft Azure Emulator 584 | ecf/ 585 | rcf/ 586 | 587 | # Windows Store app package directories and files 588 | AppPackages/ 589 | BundleArtifacts/ 590 | Package.StoreAssociation.xml 591 | _pkginfo.txt 592 | *.appx 593 | *.appxbundle 594 | *.appxupload 595 | 596 | # Visual Studio cache files 597 | # files ending in .cache can be ignored 598 | *.[Cc]ache 599 | # but keep track of directories ending in .cache 600 | !?*.[Cc]ache/ 601 | 602 | # Others 603 | ClientBin/ 604 | ~$* 605 | *.dbmdl 606 | *.dbproj.schemaview 607 | *.jfm 608 | *.pfx 609 | *.publishsettings 610 | orleans.codegen.cs 611 | 612 | # Including strong name files can present a security risk 613 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 614 | #*.snk 615 | 616 | # Since there are multiple workflows, uncomment next line to ignore bower_components 617 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 618 | #bower_components/ 619 | 620 | # RIA/Silverlight projects 621 | Generated_Code/ 622 | 623 | # Backup & report files from converting an old project file 624 | # to a newer Visual Studio version. Backup files are not needed, 625 | # because we have git ;-) 626 | _UpgradeReport_Files/ 627 | Backup*/ 628 | UpgradeLog*.XML 629 | UpgradeLog*.htm 630 | ServiceFabricBackup/ 631 | *.rptproj.bak 632 | 633 | # SQL Server files 634 | *.mdf 635 | *.ldf 636 | *.ndf 637 | 638 | # Business Intelligence projects 639 | *.rdl.data 640 | *.bim.layout 641 | *.bim_*.settings 642 | *.rptproj.rsuser 643 | *- [Bb]ackup.rdl 644 | *- [Bb]ackup ([0-9]).rdl 645 | *- [Bb]ackup ([0-9][0-9]).rdl 646 | 647 | # Microsoft Fakes 648 | FakesAssemblies/ 649 | 650 | # GhostDoc plugin setting file 651 | *.GhostDoc.xml 652 | 653 | # Node.js Tools for Visual Studio 654 | .ntvs_analysis.dat 655 | node_modules/ 656 | 657 | # Visual Studio 6 build log 658 | *.plg 659 | 660 | # Visual Studio 6 workspace options file 661 | *.opt 662 | 663 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 664 | *.vbw 665 | 666 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 667 | *.vbp 668 | 669 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 670 | *.dsw 671 | *.dsp 672 | 673 | # Visual Studio 6 technical files 674 | 675 | # Visual Studio LightSwitch build output 676 | **/*.HTMLClient/GeneratedArtifacts 677 | **/*.DesktopClient/GeneratedArtifacts 678 | **/*.DesktopClient/ModelManifest.xml 679 | **/*.Server/GeneratedArtifacts 680 | **/*.Server/ModelManifest.xml 681 | _Pvt_Extensions 682 | 683 | # Paket dependency manager 684 | .paket/paket.exe 685 | paket-files/ 686 | 687 | # FAKE - F# Make 688 | .fake/ 689 | 690 | # CodeRush personal settings 691 | .cr/personal 692 | 693 | # Python Tools for Visual Studio (PTVS) 694 | *.pyc 695 | 696 | # Cake - Uncomment if you are using it 697 | # tools/** 698 | # !tools/packages.config 699 | 700 | # Tabs Studio 701 | *.tss 702 | 703 | # Telerik's JustMock configuration file 704 | *.jmconfig 705 | 706 | # BizTalk build output 707 | *.btp.cs 708 | *.btm.cs 709 | *.odx.cs 710 | *.xsd.cs 711 | 712 | # OpenCover UI analysis results 713 | OpenCover/ 714 | 715 | # Azure Stream Analytics local run output 716 | ASALocalRun/ 717 | 718 | # MSBuild Binary and Structured Log 719 | *.binlog 720 | 721 | # NVidia Nsight GPU debugger configuration file 722 | *.nvuser 723 | 724 | # MFractors (Xamarin productivity tool) working folder 725 | .mfractor/ 726 | 727 | # Local History for Visual Studio 728 | .localhistory/ 729 | 730 | # Visual Studio History (VSHistory) files 731 | .vshistory/ 732 | 733 | # BeatPulse healthcheck temp database 734 | healthchecksdb 735 | 736 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 737 | MigrationBackup/ 738 | 739 | # Ionide (cross platform F# VS Code tools) working folder 740 | .ionide/ 741 | 742 | # Fody - auto-generated XML schema 743 | FodyWeavers.xsd 744 | 745 | # VS Code files for those working on multiple tools 746 | .vscode/* 747 | !.vscode/settings.json 748 | !.vscode/tasks.json 749 | !.vscode/launch.json 750 | !.vscode/extensions.json 751 | *.code-workspace 752 | 753 | # Local History for Visual Studio Code 754 | .history/ 755 | 756 | # Windows Installer files from build outputs 757 | 758 | # JetBrains Rider 759 | *.sln.iml 760 | 761 | ### VisualStudio Patch ### 762 | # Additional files built by Visual Studio 763 | 764 | # End of https://www.toptal.com/developers/gitignore/api/macos,linux,windows,pycharm+all,visualstudio,intellij+all,python --------------------------------------------------------------------------------