diff --git a/.gitignore b/.gitignore index 7eae2ca..0927d44 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ var/ *.egg-info/ .installed.cfg *.egg + +# Python +.coverage +coverage.xml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..464cdf7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "python.analysis.autoImportCompletions": true, + "python.testing.pytestArgs": [ + "-v", + "--cov=aiosdnotify", + "--cov-report=xml", + "--cov-report=term", + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "mypy-type-checker.args": [ + "--strict" + ] +} diff --git a/LICENSE.txt b/LICENSE.txt index efab5e1..52392b6 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Brett Bethke +Copyright 2024 Calian Ltd. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 7bda180..7836f56 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # systemd Service Notification -This is Garrett's Asyncio version of the systemd sd_notify package written by bb4242 (Brett Bethke), +This is a Calian fork of Garrett's Asyncio version (https://github.com/seatsnob/sdnotify-asyncio) +of the systemd sd_notify package written by bb4242 (Brett Bethke), available at https://github.com/bb4242/sdnotify This is a pure Python implementation of the @@ -9,11 +10,7 @@ This is a pure Python implementation of the protocol. This protocol can be used to inform `systemd` about service start-up completion, watchdog events, and other service status changes. Thus, this package can be used to write system services in Python that play nicely with -`systemd`. `sdnotify` is compatible with both Python 2 and Python 3. - -Just so we're clear, I figured out _why_ nobody made this. Sdnotify uses UDP under the hood. -There isn't an asyncio implementation of UDP. So I mostly just wound up writing shitty wrappers -for asyncio around multithreading. +`systemd`. `async-sdnotify` is compatible with Python 3. Normally the `SystemdNotifier.notify` method silently ignores exceptions (for example, if the systemd notification socket is not available) to allow applications to @@ -23,7 +20,7 @@ aid in debugging. # Installation -`pip install sdnotify-asyncio` +`pip install async-sdnotify` # Example Usage diff --git a/aiosdnotify/__init__.py b/aiosdnotify/__init__.py index 7b16f8a..c4ff314 100644 --- a/aiosdnotify/__init__.py +++ b/aiosdnotify/__init__.py @@ -1,10 +1,20 @@ +# +# Copyright 2024 Calian Ltd. All rights reserved. +# +# Original authors: +# - Copyright 2023 Seat Snob (garrett@seatsnob.com) +# - Copyright 2016 Brett Bethke (bbethke@gmail.com) +# + import socket import asyncio import os import logging +from types import TracebackType +from typing import Any, Optional, Self, Type -__version__ = "1.0.0" +__version__ = "0.1.0" logger = logging.getLogger(__name__) @@ -13,7 +23,7 @@ class SystemdNotifier: """This class holds a connection to the systemd notification socket and can be used to send messages to systemd using its notify method.""" - def __init__(self, debug=False, warn_limit=3, watchdog=True): + def __init__(self, debug: bool=False, warn_limit: int=3, watchdog: bool=True) -> None: """Instantiate a new notifier object. This will initiate a connection to the systemd notification socket. @@ -36,29 +46,31 @@ def __init__(self, debug=False, warn_limit=3, watchdog=True): self._warnings = 0 self._transport: asyncio.DatagramTransport | None = None self._protocol: asyncio.DatagramProtocol | None = None - self._watchdog_task: asyncio.Task | None = None + self._watchdog_task: asyncio.Task[None] | None = None self._ready_event = asyncio.Event() - async def connect(self): + async def connect(self) -> None: try: - notify_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) addr = os.getenv('NOTIFY_SOCKET') + if addr is None: + raise EnvironmentError('NOTIFY_SOCKET not set') + notify_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) if addr[0] == '@': addr = '\0' + addr[1:] notify_socket.connect(addr) self._transport, self._protocol = await asyncio.get_running_loop().create_datagram_endpoint( asyncio.DatagramProtocol, sock=notify_socket) - except Exception as e: + except Exception: if self.debug: raise elif self.warn_limit: logger.warning('SystemdNotifier failed to connect', exc_info=True) - def disconnect(self): + def disconnect(self) -> None: if self._transport is not None and not self._transport.is_closing(): self._transport.close() - def _notify(self, state_str: str): + def _notify(self, state_str: str) -> None: """Send a notification to systemd. state is a string; see the man page of sd_notify (http://www.freedesktop.org/software/systemd/man/sd_notify.html) for a description of the allowable values. @@ -69,6 +81,8 @@ def _notify(self, state_str: str): cause this method to raise any exceptions generated to the caller, to aid in debugging.""" try: + if self._transport is None: + raise RuntimeError('SystemdNotifier is not connected') self._transport.sendto(state_str.encode()) self._warnings = 0 except Exception: @@ -78,35 +92,40 @@ def _notify(self, state_str: str): logger.warning('SystemdNotifier failed to notify', exc_info=True) self._warnings += 1 - def ready(self): + def ready(self) -> None: self._notify("READY=1") self._ready_event.set() - def status(self, status: str): + def status(self, status: str) -> None: self._notify(f"STATUS={status}") - def start_watchdog(self, interval: float | None = None): + def start_watchdog(self, interval: float | None = None) -> None: if interval is None: - interval_microseconds = float(os.getenv('WATCHDOG_USEC')) + interval_microseconds = os.getenv('WATCHDOG_USEC') if interval_microseconds is None: - raise EnvironmentError('Unable to determine watchdog interval from ENV, and none was specified') - interval = interval_microseconds / 1_000_000 / 2 # We try to notify at half the env specified interval + message = 'Unable to determine watchdog interval from ENV, and none was specified' + if self.debug: + raise EnvironmentError(message) + else: + logger.warning(message) + return + interval = float(interval_microseconds) / 1_000_000 / 2 # We try to notify at half the env specified interval self._watchdog_task = asyncio.create_task( self._watchdog(interval)) - async def _watchdog(self, interval: float): + async def _watchdog(self, interval: float) -> None: await self._ready_event.wait() while True: self._notify('WATCHDOG=1') await asyncio.sleep(interval) - async def __aenter__(self): + async def __aenter__(self) -> Self: await self.connect() if self.watchdog: self.start_watchdog() return self - async def __aexit__(self, exc_type, exc_val, exc_tb): + async def __aexit__(self, exc_type: Type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None: if ( self._watchdog_task and not self._watchdog_task.done() diff --git a/aiosdnotify/py.typed b/aiosdnotify/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c6f7b83 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,184 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.2" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, + {file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, + {file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, + {file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, + {file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, + {file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, + {file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, + {file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, + {file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, + {file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, + {file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, + {file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, + {file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, + {file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, + {file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, + {file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "7dca67ee546c9582a336059dc8734cd09e388b02b9de26bd118ba102f70e46cd" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..efa46ec --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..35b2e78 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[tool.poetry] +name = "async-sdnotify" +version = "0.1.0" +description = "A pure asynchronous Python implementation of systemd's service notification protocol (sd_notify)" +authors = [ + "Calian Ltd. ", + "Seat Snob ", + "Brett Bethke ", +] +license = "MIT" +readme = "README.md" +classifiers = [ + "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", + "Topic :: Software Development :: Libraries :: Python Modules", +] +packages = [ + {include = "aiosdnotify"}, +] + +[tool.poetry.dependencies] +python = "^3.11" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.3.3" +pytest-asyncio = "^0.24.0" +pytest-cov = "^5.0.0" + +[tool.mypy] +mypy_path = [ + "$MYPY_CONFIG_FILE_DIR/aiosdnotify" +] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/test_systemdnotifier.py b/tests/test_systemdnotifier.py new file mode 100644 index 0000000..98ac323 --- /dev/null +++ b/tests/test_systemdnotifier.py @@ -0,0 +1,57 @@ +import asyncio +from contextlib import nullcontext as does_not_raise +import os +import socket +from typing import Iterator +import pytest +from aiosdnotify import SystemdNotifier + + +@pytest.fixture +def watchdog_socket() -> Iterator[socket.socket]: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + sock.bind('\0' + 'watchdog') + os.environ['NOTIFY_SOCKET'] = '@watchdog' + os.environ['WATCHDOG_USEC'] = '1000000' + yield sock + sock.close() + del os.environ['NOTIFY_SOCKET'] + del os.environ['WATCHDOG_USEC'] + + +@pytest.mark.asyncio +async def test_no_exceptions_without_environment() -> None: + with does_not_raise(): + async with SystemdNotifier() as notifier: + notifier.ready() + notifier.status("Testing") + + +@pytest.mark.asyncio +async def test_connect_throws_exception_when_debug() -> None: + with pytest.raises(EnvironmentError): + await SystemdNotifier(debug=True).connect() + + +def test_status_throws_exception_when_debug() -> None: + with pytest.raises(RuntimeError): + SystemdNotifier(debug=True).status("Testing") + + +def test_start_watchdog_throws_exception_when_debug() -> None: + with pytest.raises(EnvironmentError): + SystemdNotifier(debug=True).start_watchdog() + + +@pytest.mark.asyncio +async def test_sends_notifications(watchdog_socket: socket.socket) -> None: + async with SystemdNotifier() as notifier: + notifier.ready() + ready = watchdog_socket.recv(1024) + assert ready == b"READY=1" + notifier.status("Testing") + status = watchdog_socket.recv(1024) + assert status == b"STATUS=Testing" + await asyncio.sleep(1) + data = watchdog_socket.recv(1024) + assert data == b"WATCHDOG=1"