├── .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 | 
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
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 |
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
--------------------------------------------------------------------------------