From f23a14f389bc28de0481d5bba936033535ad7f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 19:02:25 +0300 Subject: [PATCH 01/14] fix and add typing info --- src/Socket_Singleton.py | 49 +++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/Socket_Singleton.py b/src/Socket_Singleton.py index d130c77..3d4a010 100644 --- a/src/Socket_Singleton.py +++ b/src/Socket_Singleton.py @@ -45,8 +45,8 @@ def __init__( release_threshold: int = 0, max_clients: int = 0, verbose: bool = False, - secret: str = None, - ): + secret: str | None = None, + ) -> None: """ Initialize the singleton instance. @@ -86,7 +86,7 @@ def __init__( # Store arguments as tuples - each tuple represents one client's complete argument set # Internally, this functions as a queue. See self.arguments() for external access. # Note: Host's own arguments are not stored here - only arguments from client processes. - self._arguments = [] + self._arguments: list[tuple[str, ...]] = [] self._observers = {} self._clients = 0 self._listening = False @@ -105,13 +105,13 @@ def __init__( self._create_client() if self.strict: - raise SystemExit - else: - raise MultipleSingletonsError( - "\nApplication is already bound & listening " - f"@ {self.address} on port {self.port}. Multiple " - f"instances are disallowed in the current context." - ) from None + raise SystemExit from err + + raise MultipleSingletonsError( + "\nApplication is already bound & listening " + f"@ {self.address} on port {self.port}. Multiple " + f"instances are disallowed in the current context." + ) from None else: self._listening = True @@ -122,12 +122,12 @@ def __init__( if self.timeout > 0: self._timer.start() - def __str__(self): + def __str__(self) -> str: """Human-readable string representation.""" return f"Socket_Singleton(address={self.address!r}, port={self.port})" - def __repr__(self): + def __repr__(self) -> str: """ Unambiguous string representation for developers. @@ -165,7 +165,7 @@ def __exit__(self, ex_type, ex_value, ex_traceback): self.release() return False - def _create_server(self): + def _create_server(self) -> None: """ Server thread that listens for client connections and processes arguments. @@ -229,9 +229,8 @@ def _create_server(self): f"Socket_Singleton: Failed to decode data from client " f"on port {self.port}, skipping arguments" ) - pass - def _create_client(self): + def _create_client(self) -> None: """ Client behavior when port is already bound. @@ -244,7 +243,7 @@ def _create_client(self): sock.connect((self.address, self.port)) # Build message: secret (if required) + arguments, joined by null bytes # Use null byte (\x00) as delimiter to avoid issues with newlines - parts = [] + parts: list[str] = [] if self.secret is not None: parts.append(self.secret) parts.extend(argv[1:]) @@ -270,9 +269,8 @@ def _create_client(self): f"Socket_Singleton: Failed to connect to existing instance " f"on {self.address}:{self.port} (port may have been released)" ) - pass - def _append_args(self, args): + def _append_args(self, args: tuple[str, ...]) -> None: """ Append a complete argument set from a client to the queue and notify observers. """ @@ -280,7 +278,7 @@ def _append_args(self, args): self._arguments.append(args) self._update_observers() - def _update_observers(self): + def _update_observers(self) -> None: """ Publish the most recent argument set to all registered observers. @@ -317,9 +315,8 @@ def _update_observers(self): f"Socket_Singleton: Observer {observer_name} " f"raised exception: {type(exc).__name__}: {exc}" ) - pass - def trace(self, observer, *args, **kwargs): + def trace(self, observer, *args, **kwargs) -> None: """ Register an observer callback to receive arguments from client processes. @@ -344,12 +341,12 @@ def my_callback(args_tuple, prefix, suffix="<<<"): self._observers[observer] = (args, kwargs) - def untrace(self, observer): + def untrace(self, observer) -> None: """Detach (unsubscribe) a callback. Does nothing if the observer is not registered.""" self._observers.pop(observer, None) - def release(self): + def release(self) -> None: """ Release the port, allowing other instances to bind. @@ -366,7 +363,7 @@ def release(self): self._listening = False - if hasattr(self, "_timer"): + if hasattr(self, "_timer") and self._timer is not None: self._timer.cancel() # No new arguments will arrive after release @@ -385,7 +382,7 @@ def release(self): pass @property - def arguments(self): + def arguments(self) -> tuple[tuple[str, ...], ...]: """ Read-only snapshot of arguments received from client processes. @@ -401,7 +398,7 @@ def arguments(self): return tuple(self._arguments) @property - def clients(self): + def clients(self) -> int: """ Number of client processes that have connected since this singleton was created. From cd9a3bcdd6722790e133175e9b6eecd97c44a708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 19:44:38 +0300 Subject: [PATCH 02/14] renamed readme.md to README_.md --- readme.md => README_.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename readme.md => README_.md (99%) diff --git a/readme.md b/README_.md similarity index 99% rename from readme.md rename to README_.md index 895cead..1933dc7 100644 --- a/readme.md +++ b/README_.md @@ -59,7 +59,7 @@ The interpreter exits immediately and we end up back at the prompt. **See also:** -[Common TCP/UDP Port Numbers](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers) +[Common TCP/UDP Port Numbers](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers) [Windows Socket Error Code 10048](https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#WSAEADDRINUSE) It is recommended to specify a port in the constructor\* From da47308c52781d17956c10f52f93047965c6992a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 19:45:01 +0300 Subject: [PATCH 03/14] renamed README_.md to README.md --- README_.md => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README_.md => README.md (100%) diff --git a/README_.md b/README.md similarity index 100% rename from README_.md rename to README.md From de8bcad4eefec143972df72397504b48193a433d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 19:46:59 +0300 Subject: [PATCH 04/14] moved Socket_Singleton.py into the src/Socket_Singleton dir --- pyproject.toml | 41 ++++++++++++++++--- .../Socket_Singleton.py | 7 ++-- src/Socket_Singleton/__init__.py | 6 +++ test_app.py | 2 +- tests.py | 2 +- 5 files changed, 47 insertions(+), 11 deletions(-) rename src/{ => Socket_Singleton}/Socket_Singleton.py (98%) create mode 100644 src/Socket_Singleton/__init__.py diff --git a/pyproject.toml b/pyproject.toml index c46eeef..2672142 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,34 @@ +[project] +authors = [ + {name = "Emboiko", email = "ed@emboiko.com"}, +] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", +] +dependencies = [] +description = "Allow a single instance of a Python application to run at once" +license = "MIT" +license-files = ["LICEN[CS]E.*"] +name = "Socket_Singleton" +readme = "README.md" +requires-python = ">=3.7" +version = "2.0.1" + +[project.urls] +Repository = "https://github.com/emboiko/Socket_Singleton" + +[build-system] +build-backend = "uv_build" +requires = ["uv_build>=0.11.7,<0.12.0"] + [tool.black] -line-length = 100 -target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312'] -include = '\.pyi?$' extend-exclude = ''' /( # directories @@ -18,10 +45,12 @@ extend-exclude = ''' | __pycache__ )/ ''' +include = '\.pyi?$' +line-length = 100 +target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312'] [tool.isort] -profile = "black" -line_length = 100 known_first_party = ["Socket_Singleton"] +line_length = 100 +profile = "black" skip_glob = ["*/__pycache__/*", "*/build/*", "*/dist/*"] - diff --git a/src/Socket_Singleton.py b/src/Socket_Singleton/Socket_Singleton.py similarity index 98% rename from src/Socket_Singleton.py rename to src/Socket_Singleton/Socket_Singleton.py index 3d4a010..e141118 100644 --- a/src/Socket_Singleton.py +++ b/src/Socket_Singleton/Socket_Singleton.py @@ -1,6 +1,7 @@ import errno from socket import socket from sys import argv +from typing import Any, Callable from threading import Thread, Timer _WSAEADDRINUSE = 10048 @@ -87,7 +88,7 @@ def __init__( # Internally, this functions as a queue. See self.arguments() for external access. # Note: Host's own arguments are not stored here - only arguments from client processes. self._arguments: list[tuple[str, ...]] = [] - self._observers = {} + self._observers: dict[Callable, tuple[tuple[Any, ...], dict[str, Any]]] = {} self._clients = 0 self._listening = False self._thread = None @@ -316,7 +317,7 @@ def _update_observers(self) -> None: f"raised exception: {type(exc).__name__}: {exc}" ) - def trace(self, observer, *args, **kwargs) -> None: + def trace(self, observer: Callable, *args: Any, **kwargs: Any) -> None: """ Register an observer callback to receive arguments from client processes. @@ -341,7 +342,7 @@ def my_callback(args_tuple, prefix, suffix="<<<"): self._observers[observer] = (args, kwargs) - def untrace(self, observer) -> None: + def untrace(self, observer: Callable) -> None: """Detach (unsubscribe) a callback. Does nothing if the observer is not registered.""" self._observers.pop(observer, None) diff --git a/src/Socket_Singleton/__init__.py b/src/Socket_Singleton/__init__.py new file mode 100644 index 0000000..2e846ba --- /dev/null +++ b/src/Socket_Singleton/__init__.py @@ -0,0 +1,6 @@ +from .Socket_Singleton import Socket_Singleton, MultipleSingletonsError + +__all__ = [ + "Socket_Singleton", + "MultipleSingletonsError", +] diff --git a/test_app.py b/test_app.py index 7422f42..6c6bd83 100644 --- a/test_app.py +++ b/test_app.py @@ -2,7 +2,7 @@ from sys import argv from time import sleep -from src.Socket_Singleton import MultipleSingletonsError, Socket_Singleton +from Socket_Singleton import MultipleSingletonsError, Socket_Singleton # This file is used for the test cases in test.py and for manual # debugging / testing, primarily as a subprocess helper. diff --git a/tests.py b/tests.py index 8e59ffa..7da8897 100644 --- a/tests.py +++ b/tests.py @@ -14,7 +14,7 @@ from subprocess import PIPE, STDOUT, Popen, run from time import sleep -from src.Socket_Singleton import MultipleSingletonsError, Socket_Singleton +from Socket_Singleton import MultipleSingletonsError, Socket_Singleton def get_free_port(): From 15841ab304534ab6d11166d0faabb4eaf491b5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 19:47:18 +0300 Subject: [PATCH 05/14] added uv.lock --- uv.lock | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 uv.lock diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..0fe70b8 --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.7" + +[[package]] +name = "socket-singleton" +version = "2.0.1" +source = { editable = "." } From 6cea4ade5132164d2c3c63b8f311491aafa79862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 19:47:33 +0300 Subject: [PATCH 06/14] disable flake8 in vscode --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 851f1da..ee4b482 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,5 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "ms-python.black-formatter" }, + "flake8.enabled": false, } - From ae5d53e303d53e8a77b44190538ff520d5608040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 20:10:56 +0300 Subject: [PATCH 07/14] update the README; use pytest for driving tests --- README.md | 41 +-- pyproject.toml | 10 + test_app.py => tests/helper_app.py | 0 tests.py => tests/test_socket_singleton.py | 8 +- uv.lock | 367 +++++++++++++++++++++ 5 files changed, 400 insertions(+), 26 deletions(-) rename test_app.py => tests/helper_app.py (100%) rename tests.py => tests/test_socket_singleton.py (99%) diff --git a/README.md b/README.md index 1933dc7..2fce2b7 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ -## Socket_Singleton.py +# Socket_Singleton.py -### Socket-based, single-instance Python applications with a clean interface +Socket-based, single-instance Python applications with a clean interface. -###### _Without lockfiles, mutexes, dependencies, or tomfoolery_ +_Without lockfiles, mutexes, dependencies, or tomfoolery._ -### Installation & Basic Usage +## Installation & Basic Usage **Install:** -`pip install Socket_Singleton -U` +```bash +pip install Socket_Singleton -U +``` **Import:** -`from Socket_Singleton import Socket_Singleton` +```python +from Socket_Singleton import Socket_Singleton +``` **Basic Usage:** @@ -253,7 +257,6 @@ app = Socket_Singleton(secret=secret) **Important:** Both host and client processes must use the same `secret` value. If they don't match, the client's arguments will be ignored. - ## Methods ### `trace(observer, *args, **kwargs)` @@ -267,6 +270,7 @@ When you register an observer with `trace()`, you can optionally provide additio **Observer signature:** Your observer callback receives arguments in this order: + 1. **First parameter**: A tuple containing all arguments from a single client process 2. **Followed by**: Any `*args` you provided to `trace()` (unpacked) 3. **Followed by**: Any `**kwargs` you provided to `trace()` (unpacked) @@ -407,7 +411,6 @@ An integer property describing how many client processes have connected since in print(f"Connected clients: {app.clients}") ``` - ## Context Manager The context manager protocol is implemented for automatic resource cleanup: @@ -427,43 +430,35 @@ with Socket_Singleton() as ss: The port is automatically released when exiting the `with` block. - ## Testing -The project includes a comprehensive test suite using Python's built-in `unittest` framework. +The project includes a comprehensive test suite using `pytest`. **Run all tests:** ```bash -python -m unittest tests -``` - -**Run tests with verbose output:** - -```bash -python -m unittest -v tests +pytest ``` **Run a specific test class:** ```bash -python -m unittest tests.TestInProcess -python -m unittest tests.TestArgumentPassing -python -m unittest tests.TestConcurrency +pytest tests/test_socket_singleton.py::TestInProcess ``` **Run a specific test method:** ```bash -python -m unittest tests.TestArgumentPassing.test_multiple_observers +pytest tests/test_socket_singleton.py::TestInProcess::test_arguments_with_newlines ``` **Test structure:** -- `tests.py` - Main test suite with organized test classes -- `test_app.py` - Helper script for subprocess-based tests +- `test_socket_singleton.py` - Main test suite with organized test classes +- `helper_app.py` - Helper script for subprocess-based tests Tests are organized by concern: + - **TestInProcess**: Fast in-process tests (properties, trace/untrace, context manager) - **TestSingletonEnforcement**: Singleton behavior requiring separate processes - **TestArgumentPassing**: Argument passing between processes diff --git a/pyproject.toml b/pyproject.toml index 2672142..11bff6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,3 +54,13 @@ known_first_party = ["Socket_Singleton"] line_length = 100 profile = "black" skip_glob = ["*/__pycache__/*", "*/build/*", "*/dist/*"] + +[dependency-groups] +dev = [ + "pytest>=7.4.4", +] + +[tool.pytest.ini_options] +log_cli = true +log_level = "NOTSET" +testpaths = ["tests"] diff --git a/test_app.py b/tests/helper_app.py similarity index 100% rename from test_app.py rename to tests/helper_app.py diff --git a/tests.py b/tests/test_socket_singleton.py similarity index 99% rename from tests.py rename to tests/test_socket_singleton.py index 7da8897..af1538c 100644 --- a/tests.py +++ b/tests/test_socket_singleton.py @@ -11,6 +11,7 @@ import socket import unittest +from pathlib import Path from subprocess import PIPE, STDOUT, Popen, run from time import sleep @@ -36,7 +37,8 @@ def run_test_app(command, wait=True, capture_output=False): Returns: CompletedProcess if wait=True, Popen if wait=False """ - cmd = f"python test_app.py {command}" + app_path = Path(__file__).parent / "helper_app.py" + cmd = f"python {app_path} {command}" if wait: return run(cmd, shell=True, capture_output=True, text=True) @@ -836,5 +838,5 @@ def test_rapid_successive_launches(self): self.assertTrue(found, f"Missing arguments from rapid{i}") -if __name__ == "__main__": - unittest.main() +# if __name__ == "__main__": +# unittest.main() diff --git a/uv.lock b/uv.lock index 0fe70b8..1d1ba21 100644 --- a/uv.lock +++ b/uv.lock @@ -1,8 +1,375 @@ version = 1 revision = 3 requires-python = ">=3.7" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "zipp", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569, upload-time = "2023-06-18T21:44:35.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934, upload-time = "2023-06-18T21:44:33.441Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "24.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", size = 61613, upload-time = "2023-06-21T09:12:28.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", size = 17695, upload-time = "2023-06-21T09:12:27.397Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.8' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, + { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "packaging", version = "24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pluggy", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.8.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.8.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pygments", marker = "python_full_version == '3.9.*'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] [[package]] name = "socket-singleton" version = "2.0.1" source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=7.4.4" }] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/8b/0111dd7d6c1478bf83baa1cab85c686426c7a6274119aceb2bd9d35395ad/typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2", size = 72876, upload-time = "2023-07-02T14:20:55.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6b/63cc3df74987c36fe26157ee12e09e8f9db4de771e0f3404263117e75b95/typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", size = 33232, upload-time = "2023-07-02T14:20:53.275Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "zipp" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454, upload-time = "2023-02-25T02:17:22.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758, upload-time = "2023-02-25T02:17:20.807Z" }, +] From b4c45e6295fa53467fa4b1fc1b324fcb762ad12f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 20:18:24 +0300 Subject: [PATCH 08/14] updated the README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 2fce2b7..0af8ca1 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ secret = os.getenv("SOCKET_SINGLETON_SECRET") app = Socket_Singleton(secret=secret) ``` -**How it works interally:** +**How it works internally:** - If `secret` is `None` (default): No verification - any connection is accepted by the host - If `secret` is provided to the host: Clients must send the secret as the first part of their message over the socket (before a null byte `\x00`), followed by arguments from their process @@ -391,7 +391,6 @@ And in a new shell (after `release()` was called): - **Context manager alternative**: For most use cases, the context manager protocol (see below) is cleaner and automatically handles cleanup. - **Timer cancellation**: If a timeout was set, calling `release()` will cancel it prematurely. - ## Properties ### `arguments` From 7f8ed9740a9eee919a5168e288724780ca8a9b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 20:23:54 +0300 Subject: [PATCH 09/14] added `trace_no_args` option to `Socket_Singleton` --- src/Socket_Singleton/Socket_Singleton.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Socket_Singleton/Socket_Singleton.py b/src/Socket_Singleton/Socket_Singleton.py index e141118..872b5cd 100644 --- a/src/Socket_Singleton/Socket_Singleton.py +++ b/src/Socket_Singleton/Socket_Singleton.py @@ -34,6 +34,9 @@ class Socket_Singleton: secret: Optional secret string for client verification. If provided, clients must send this secret before their arguments. Defaults to None (no verification). Useful for preventing unauthorized applications from injecting arguments. + trace_no_args: If False (the default), the trace callbacks will not be executed + when a client process is called without any args. If True, the trace callbacks + will be executed even if the client process is called with no args. """ def __init__( @@ -47,6 +50,7 @@ def __init__( max_clients: int = 0, verbose: bool = False, secret: str | None = None, + trace_no_args: bool = False, ) -> None: """ Initialize the singleton instance. @@ -74,6 +78,7 @@ def __init__( self.max_clients = int(max_clients) self.verbose = bool(verbose) self.secret = str(secret) if secret is not None else None + self.trace_no_args = bool(trace_no_args) if not (0 <= self.port <= 65535): raise ValueError("port must be between 0 and 65535 (inclusive)") @@ -221,7 +226,7 @@ def _create_server(self) -> None: # Filter out empty strings args = tuple(arg for arg in parts if arg) - if args: + if args or self.trace_no_args: self._append_args(args) except (UnicodeDecodeError, AttributeError): # Invalid data received - skip this client's arguments From abd4b60dad23ffda5d347ccba99d3ecf009b9cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 20:28:27 +0300 Subject: [PATCH 10/14] ignore .venv --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8a98bab..1edb407 100644 --- a/.gitignore +++ b/.gitignore @@ -107,4 +107,6 @@ dmypy.json # Pyre type checker .pyre/ -# End of https://www.gitignore.io/api/python \ No newline at end of file +# End of https://www.gitignore.io/api/python + +.venv From 07f092c8aa817125e762d6e84dd10a33350c3121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 20:28:44 +0300 Subject: [PATCH 11/14] remove old publishing files --- MANIFEST.in | 4 ---- publish.bat | 29 ----------------------------- publish.sh | 29 ----------------------------- setup.py | 34 ---------------------------------- 4 files changed, 96 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 publish.bat delete mode 100644 publish.sh delete mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 5f26fd5..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include readme.md -include LICENSE.txt -include pyproject.toml - diff --git a/publish.bat b/publish.bat deleted file mode 100644 index e934a64..0000000 --- a/publish.bat +++ /dev/null @@ -1,29 +0,0 @@ -@echo off -REM Publish Socket_Singleton to PyPI -REM Usage: publish.bat - -echo Installing build dependencies... -pip install --upgrade build twine - -echo Cleaning previous builds... -if exist dist rmdir /s /q dist -if exist build rmdir /s /q build -if exist *.egg-info rmdir /s /q *.egg-info - -echo Building package... -python -m build - -echo Checking package... -twine check dist/* - -echo. -echo Build complete! Files created in dist/: -dir dist - -echo. -echo To upload to PyPI, run: -echo twine upload dist/* -echo. -echo To upload to TestPyPI first (recommended), run: -echo twine upload --repository testpypi dist/* - diff --git a/publish.sh b/publish.sh deleted file mode 100644 index 9f2e4e4..0000000 --- a/publish.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# Publish Socket_Singleton to PyPI -# Usage: ./publish.sh - -set -e - -echo "Installing build dependencies..." -pip install --upgrade build twine - -echo "Cleaning previous builds..." -rm -rf dist/ build/ *.egg-info - -echo "Building package..." -python -m build - -echo "Checking package..." -twine check dist/* - -echo "" -echo "Build complete! Files created in dist/:" -ls -lh dist/ - -echo "" -echo "To upload to PyPI, run:" -echo " twine upload dist/*" -echo "" -echo "To upload to TestPyPI first (recommended), run:" -echo " twine upload --repository testpypi dist/*" - diff --git a/setup.py b/setup.py deleted file mode 100644 index 6085142..0000000 --- a/setup.py +++ /dev/null @@ -1,34 +0,0 @@ -from pathlib import Path -from setuptools import setup - -# Get the directory containing setup.py -here = Path(__file__).parent -readme_path = here / "readme.md" - -with open(readme_path, encoding="utf-8") as readme: - long_description = readme.read() - -setup( - name="Socket_Singleton", - version="2.0.0", - description="Allow a single instance of a Python application to run at once", - py_modules=["Socket_Singleton"], - package_dir={"": "src"}, - license="MIT", - classifiers=[ - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/emboiko/Socket_Singleton", - author="Emboiko", - author_email="ed@emboiko.com", -) From bac80202b7e2180d59a6d9fbfa215facd377fc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 20:34:23 +0300 Subject: [PATCH 12/14] renamed src/Socket_Singleton to socket_singleton --- src/{Socket_Singleton => socket_singleton_}/Socket_Singleton.py | 0 src/{Socket_Singleton => socket_singleton_}/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{Socket_Singleton => socket_singleton_}/Socket_Singleton.py (100%) rename src/{Socket_Singleton => socket_singleton_}/__init__.py (100%) diff --git a/src/Socket_Singleton/Socket_Singleton.py b/src/socket_singleton_/Socket_Singleton.py similarity index 100% rename from src/Socket_Singleton/Socket_Singleton.py rename to src/socket_singleton_/Socket_Singleton.py diff --git a/src/Socket_Singleton/__init__.py b/src/socket_singleton_/__init__.py similarity index 100% rename from src/Socket_Singleton/__init__.py rename to src/socket_singleton_/__init__.py From d521ce8acadcd695f39909592d00790e75a809d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 20:44:44 +0300 Subject: [PATCH 13/14] renamed src/socket_singleton_ to src/socket_singleton; renamed Socket_Singleton to SocketSingleton; updated the README --- README.md | 68 +++++++++---------- pyproject.toml | 2 +- .../__init__.py} | 2 +- src/socket_singleton_/__init__.py | 6 -- tests/helper_app.py | 28 ++++---- tests/test_socket_singleton.py | 46 ++++++------- 6 files changed, 73 insertions(+), 79 deletions(-) rename src/{socket_singleton_/Socket_Singleton.py => socket_singleton/__init__.py} (99%) delete mode 100644 src/socket_singleton_/__init__.py diff --git a/README.md b/README.md index 0af8ca1..27aee79 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Socket_Singleton.py +# socket_singleton Socket-based, single-instance Python applications with a clean interface. @@ -15,20 +15,20 @@ pip install Socket_Singleton -U **Import:** ```python -from Socket_Singleton import Socket_Singleton +from socket_singleton import SocketSingleton ``` **Basic Usage:** ```python # Simple singleton enforcement -Socket_Singleton() +SocketSingleton() ``` or, keep a reference: ```python -app = Socket_Singleton() +app = SocketSingleton() ``` **Basic Example:** @@ -38,8 +38,8 @@ We have an application, `app.py` that we want to restrict to a single instance: ```python #app.py -from Socket_Singleton import Socket_Singleton -Socket_Singleton() +from socket_singleton import SocketSingleton +SocketSingleton() input() # Blocking call to simulate your_business_logic() ``` @@ -72,7 +72,7 @@ It is recommended to specify a port in the constructor\* **Constructor:** -`Socket_Singleton(address="127.0.0.1", port=1337, timeout=0, client=True, strict=True, release_threshold=0, max_clients=0, verbose=False, secret=None)` +`SocketSingleton(address="127.0.0.1", port=1337, timeout=0, client=True, strict=True, release_threshold=0, max_clients=0, verbose=False, secret=None)` ### `address` @@ -82,9 +82,9 @@ The IP address to bind the socket to. Defaults to `"127.0.0.1"` (localhost). Thi ```python # Binds to localhost - machine-wide singleton -app = Socket_Singleton() +app = SocketSingleton() # or explicitly: -app = Socket_Singleton(address="127.0.0.1") +app = SocketSingleton(address="127.0.0.1") ``` **Multi-homed systems example:** @@ -97,10 +97,10 @@ On a system with multiple network interfaces, you can create separate singleton # Interface 2: 10.0.0.50 (VPN network) # Internal network singleton -internal_app = Socket_Singleton(address="192.168.1.100", port=1337) +internal_app = SocketSingleton(address="192.168.1.100", port=1337) # VPN network singleton (separate instance) -vpn_app = Socket_Singleton(address="10.0.0.50", port=1337) +vpn_app = SocketSingleton(address="10.0.0.50", port=1337) # These can coexist because they're on different interfaces! ``` @@ -111,13 +111,13 @@ In containerized environments, you might want per-container singletons: ```python # Container A -app_a = Socket_Singleton(address="172.17.0.2", port=1337) +app_a = SocketSingleton(address="172.17.0.2", port=1337) # Container B -app_b = Socket_Singleton(address="172.17.0.3", port=1337) +app_b = SocketSingleton(address="172.17.0.3", port=1337) # Host machine -host_app = Socket_Singleton(address="127.0.0.1", port=1337) +host_app = SocketSingleton(address="127.0.0.1", port=1337) # All three can run simultaneously on different addresses ``` @@ -128,7 +128,7 @@ You can bind to all available interfaces using `"0.0.0.0"`: ```python # Binds to all network interfaces -app = Socket_Singleton(address="0.0.0.0", port=1337) +app = SocketSingleton(address="0.0.0.0", port=1337) ``` Note: For most applications, the default `127.0.0.1` (localhost) is what you want - a machine-wide singleton instance. The `address` parameter provides flexibility for specialized network configurations. @@ -150,11 +150,11 @@ If `False`, client processes won't send arguments to the host. Defaults to `True If `False`, raises `MultipleSingletonsError` instead of `SystemExit` when a second instance tries to run. Defaults to `True`. ```python -from Socket_Singleton import Socket_Singleton, MultipleSingletonsError +from socket_singleton import SocketSingleton, MultipleSingletonsError def main(): try: - app = Socket_Singleton(strict=False) + app = SocketSingleton(strict=False) except MultipleSingletonsError as err: print("We are not the singleton.") print(err) @@ -173,7 +173,7 @@ Release the port after this many client connections. Defaults to `0` (never rele ```python # Stop accepting connections after 10 clients -app = Socket_Singleton(release_threshold=10) +app = SocketSingleton(release_threshold=10) ``` ### `max_clients` @@ -182,7 +182,7 @@ Stop processing arguments after this many client connections. Defaults to `0` (p ```python # Rate limit: ignore arguments after 5 clients, but keep accepting connections -app = Socket_Singleton(max_clients=5) +app = SocketSingleton(max_clients=5) ``` **Combined usage:** @@ -191,7 +191,7 @@ You can use both parameters together for more complex scenarios: ```python # Throttle arguments at 5 clients, stop accepting at 10 clients -app = Socket_Singleton(max_clients=5, release_threshold=10) +app = SocketSingleton(max_clients=5, release_threshold=10) ``` **Important:** When using both parameters together: @@ -206,10 +206,10 @@ Enable verbose output for debugging. When `True`, prints warnings for connection ```python # Silent operation (default) -app = Socket_Singleton() +app = SocketSingleton() # Verbose mode - prints warnings for errors -app = Socket_Singleton(verbose=True) +app = SocketSingleton(verbose=True) ``` **When verbose mode is enabled, you'll see warnings for:** @@ -225,28 +225,28 @@ Optional secret string for client verification. If provided, clients must send t **Security Note:** -By default, `Socket_Singleton` accepts connections from any process that can connect to the port. This is fine for localhost-only singleton enforcement, but if you're concerned about unauthorized applications connecting and injecting arguments, you can use the `secret` parameter. +By default, `SocketSingleton` accepts connections from any process that can connect to the port. This is fine for localhost-only singleton enforcement, but if you're concerned about unauthorized applications connecting and injecting arguments, you can use the `secret` parameter. **Basic usage:** ```python # Host process -app = Socket_Singleton(secret="my-secret-key") +app = SocketSingleton(secret="my-secret-key") app.trace(callback) # Client processes (must use same secret) -Socket_Singleton(secret="my-secret-key") # Will send secret + args from the process +SocketSingleton(secret="my-secret-key") # Will send secret + args from the process ``` **Using environment variables:** ```python import os -from Socket_Singleton import Socket_Singleton +from socket_singleton import SocketSingleton # Read secret from environment variable secret = os.getenv("SOCKET_SINGLETON_SECRET") -app = Socket_Singleton(secret=secret) +app = SocketSingleton(secret=secret) ``` **How it works internally:** @@ -280,7 +280,7 @@ Your observer callback receives arguments in this order: ```python #app.py -from Socket_Singleton import Socket_Singleton +from socket_singleton import SocketSingleton def callback(client_args, prefix="Received: "): # client_args is a tuple like ("foo", "bar", "baz") @@ -289,7 +289,7 @@ def callback(client_args, prefix="Received: "): # do_a_thing(client_args) def main(): - app = Socket_Singleton() + app = SocketSingleton() app.trace(callback, prefix=">>> ") # Store "prefix" to be passed later input() # Blocking call to simulate your_business_logic() @@ -356,10 +356,10 @@ Release the port, allowing other instances to bind. Stops the server thread, can ```python #app.py -from Socket_Singleton import Socket_Singleton +from socket_singleton import SocketSingleton def main(): - app = Socket_Singleton() + app = SocketSingleton() # Do some work... app.release() # Release the port, allowing other instances to run print("Port released - other instances can now bind!") @@ -415,14 +415,14 @@ print(f"Connected clients: {app.clients}") The context manager protocol is implemented for automatic resource cleanup: ```python -with Socket_Singleton(): +with SocketSingleton(): input() # Blocking call to simulate your_business_logic() ``` -`Socket_Singleton.__enter__()` returns `self` so you can have access to the object if needed: +`SocketSingleton.__enter__()` returns `self` so you can have access to the object if needed: ```python -with Socket_Singleton() as ss: +with SocketSingleton() as ss: ss.trace(callback) input() # Blocking call to simulate your_business_logic() ``` diff --git a/pyproject.toml b/pyproject.toml index 11bff6f..60cc118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ license-files = ["LICEN[CS]E.*"] name = "Socket_Singleton" readme = "README.md" requires-python = ">=3.7" -version = "2.0.1" +version = "2.0.2" [project.urls] Repository = "https://github.com/emboiko/Socket_Singleton" diff --git a/src/socket_singleton_/Socket_Singleton.py b/src/socket_singleton/__init__.py similarity index 99% rename from src/socket_singleton_/Socket_Singleton.py rename to src/socket_singleton/__init__.py index 872b5cd..da92992 100644 --- a/src/socket_singleton_/Socket_Singleton.py +++ b/src/socket_singleton/__init__.py @@ -7,7 +7,7 @@ _WSAEADDRINUSE = 10048 -class Socket_Singleton: +class SocketSingleton: """ Enforces a single instance of a Python application using socket binding. diff --git a/src/socket_singleton_/__init__.py b/src/socket_singleton_/__init__.py deleted file mode 100644 index 2e846ba..0000000 --- a/src/socket_singleton_/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .Socket_Singleton import Socket_Singleton, MultipleSingletonsError - -__all__ = [ - "Socket_Singleton", - "MultipleSingletonsError", -] diff --git a/tests/helper_app.py b/tests/helper_app.py index 6c6bd83..9dfd10a 100644 --- a/tests/helper_app.py +++ b/tests/helper_app.py @@ -2,7 +2,7 @@ from sys import argv from time import sleep -from Socket_Singleton import MultipleSingletonsError, Socket_Singleton +from socket_singleton import MultipleSingletonsError, SocketSingleton # This file is used for the test cases in test.py and for manual # debugging / testing, primarily as a subprocess helper. @@ -30,9 +30,9 @@ def default(port=None): # Create singleton - it will read the modified argv[1:] try: if port is not None: - Socket_Singleton(port=port) + SocketSingleton(port=port) else: - Socket_Singleton() + SocketSingleton() finally: # Note: We don't restore argv here because the modification is intentional # and needed for _create_client() to send the correct arguments @@ -42,7 +42,7 @@ def default(port=None): def timeout(seconds, port): - Socket_Singleton(timeout=seconds, port=port) + SocketSingleton(timeout=seconds, port=port) def callback(args_tuple): @@ -51,7 +51,7 @@ def callback(args_tuple): def trace(seconds): # Manual testing helper - blocks thread on purpose - app = Socket_Singleton() + app = SocketSingleton() print("Singleton locked") app.trace(callback) sleep(seconds) @@ -60,9 +60,9 @@ def trace(seconds): def no_strict(port=None): try: if port is not None: - Socket_Singleton(port=port, strict=False) + SocketSingleton(port=port, strict=False) else: - Socket_Singleton(strict=False) + SocketSingleton(strict=False) print("Singleton locked") except MultipleSingletonsError: @@ -70,7 +70,7 @@ def no_strict(port=None): def release(seconds, port): - app = Socket_Singleton(port=port) + app = SocketSingleton(port=port) app.release() sleep(seconds) @@ -86,7 +86,7 @@ def verbose_host(port, wait_seconds=1): def bad_callback(args_tuple): raise ValueError("Intentional test exception") - app = Socket_Singleton(port=port, verbose=True) + app = SocketSingleton(port=port, verbose=True) app.trace(bad_callback) print("Host ready") # Keep running momentarily to receive client connections @@ -94,7 +94,7 @@ def bad_callback(args_tuple): def context(): - with Socket_Singleton(): + with SocketSingleton(): print("Singleton locked") @@ -111,13 +111,13 @@ def no_client(port=None): # Create singleton with client=False if port is not None: - Socket_Singleton(port=port, client=False) + SocketSingleton(port=port, client=False) else: - Socket_Singleton(client=False) + SocketSingleton(client=False) def max_clients(): - app = Socket_Singleton(max_clients=3) + app = SocketSingleton(max_clients=3) app.trace(callback) input() @@ -181,7 +181,7 @@ def main(): verbose_host(port, wait_seconds) elif command == "debug": # Debug block - hardcode your debugging scenario here for use with a debugger: - Socket_Singleton() + SocketSingleton() if __name__ == "__main__": diff --git a/tests/test_socket_singleton.py b/tests/test_socket_singleton.py index af1538c..83dfedb 100644 --- a/tests/test_socket_singleton.py +++ b/tests/test_socket_singleton.py @@ -15,7 +15,7 @@ from subprocess import PIPE, STDOUT, Popen, run from time import sleep -from Socket_Singleton import MultipleSingletonsError, Socket_Singleton +from socket_singleton import MultipleSingletonsError, SocketSingleton def get_free_port(): @@ -55,7 +55,7 @@ class TestInProcess(unittest.TestCase): def setUp(self): """Use a unique port for each test to avoid conflicts.""" self.port = get_free_port() - self.app = Socket_Singleton(port=self.port) + self.app = SocketSingleton(port=self.port) self.traced_args = [] def tearDown(self): @@ -138,12 +138,12 @@ def test_release_idempotency(self): def test_context_manager(self): """Test context manager protocol.""" port = get_free_port() - with Socket_Singleton(port=port) as app: + with SocketSingleton(port=port) as app: self.assertTrue(app._listening) - self.assertIsInstance(app, Socket_Singleton) + self.assertIsInstance(app, SocketSingleton) # Should be able to bind again - app2 = Socket_Singleton(port=port) + app2 = SocketSingleton(port=port) self.assertTrue(app2._listening) app2.release() @@ -153,7 +153,7 @@ def test_secret_verification(self): secret = "test-secret-123" # Create host with secret - host = Socket_Singleton(port=port, secret=secret) + host = SocketSingleton(port=port, secret=secret) received_args = [] def callback(args_tuple): @@ -202,7 +202,7 @@ def callback(args_tuple): def test_arguments_with_newlines(self): """Test that arguments containing newlines are handled correctly.""" port = get_free_port() - host = Socket_Singleton(port=port) + host = SocketSingleton(port=port) received_args = [] def callback(args_tuple): @@ -236,7 +236,7 @@ def callback(args_tuple): def test_string_representations(self): """Test __str__ and __repr__ methods.""" port = get_free_port() - app = Socket_Singleton(port=port, timeout=10, verbose=True) + app = SocketSingleton(port=port, timeout=10, verbose=True) # Test __str__ - should be human-readable str_repr = str(app) @@ -274,7 +274,7 @@ def test_string_representations(self): app.release() # Test __repr__ with secret (should be masked) - app_with_secret = Socket_Singleton(port=get_free_port(), secret="my-secret") + app_with_secret = SocketSingleton(port=get_free_port(), secret="my-secret") repr_with_secret = repr(app_with_secret) self.assertIn("secret=***", repr_with_secret) # Secret should be masked self.assertNotIn("my-secret", repr_with_secret) # Actual secret should not appear @@ -287,25 +287,25 @@ class TestValidation(unittest.TestCase): def test_invalid_port_too_low(self): """Test that port < 0 raises ValueError.""" with self.assertRaises(ValueError) as context: - Socket_Singleton(port=-1) + SocketSingleton(port=-1) self.assertIn("port must be between 0 and 65535", str(context.exception)) def test_invalid_port_too_high(self): """Test that port > 65535 raises ValueError.""" with self.assertRaises(ValueError) as context: - Socket_Singleton(port=65536) + SocketSingleton(port=65536) self.assertIn("port must be between 0 and 65535", str(context.exception)) def test_invalid_timeout(self): """Test that timeout < 0 raises ValueError.""" with self.assertRaises(ValueError) as context: - Socket_Singleton(port=get_free_port(), timeout=-1) + SocketSingleton(port=get_free_port(), timeout=-1) self.assertIn("timeout must be greater than or equal to 0", str(context.exception)) def test_invalid_release_threshold(self): """Test that release_threshold < 0 raises ValueError.""" with self.assertRaises(ValueError) as context: - Socket_Singleton(port=get_free_port(), release_threshold=-1) + SocketSingleton(port=get_free_port(), release_threshold=-1) self.assertIn( "release_threshold must be greater than or equal to 0", str(context.exception) ) @@ -313,7 +313,7 @@ def test_invalid_release_threshold(self): def test_invalid_max_clients(self): """Test that max_clients < 0 raises ValueError.""" with self.assertRaises(ValueError) as context: - Socket_Singleton(port=get_free_port(), max_clients=-1) + SocketSingleton(port=get_free_port(), max_clients=-1) self.assertIn("max_clients must be greater than or equal to 0", str(context.exception)) @@ -324,7 +324,7 @@ def setUp(self): """Use a unique port for each test.""" self.port = get_free_port() # Create a singleton in this process - self.app = Socket_Singleton(port=self.port) + self.app = SocketSingleton(port=self.port) def tearDown(self): """Clean up after each test.""" @@ -377,7 +377,7 @@ def test_no_strict_mode(self): # Try to create another instance in the same process with strict=False # This should raise MultipleSingletonsError with self.assertRaises(MultipleSingletonsError) as context: - Socket_Singleton(port=self.port, strict=False) + SocketSingleton(port=self.port, strict=False) # Verify the error message self.assertIn("already bound & listening", str(context.exception)) @@ -394,7 +394,7 @@ class TestArgumentPassing(unittest.TestCase): def setUp(self): """Set up singleton with observer.""" self.port = get_free_port() - self.app = Socket_Singleton(port=self.port) + self.app = SocketSingleton(port=self.port) self.received_args = [] def callback(args_tuple): @@ -443,7 +443,7 @@ def test_no_client_mode(self): self.app.release() # Create new host singleton (normal, client=True doesn't matter for host) - self.app = Socket_Singleton(port=self.port) + self.app = SocketSingleton(port=self.port) self.received_args = [] def callback(args_tuple): @@ -639,7 +639,7 @@ def setUp(self): def callback(args_tuple): self.received_args.append(args_tuple) - self.app = Socket_Singleton(port=self.port) + self.app = SocketSingleton(port=self.port) self.app.trace(callback) def tearDown(self): @@ -652,7 +652,7 @@ def test_max_clients(self): self.app.release() # Create new singleton with max_clients=2 - self.app = Socket_Singleton(port=self.port, max_clients=2) + self.app = SocketSingleton(port=self.port, max_clients=2) self.received_args = [] def callback(args_tuple): @@ -675,7 +675,7 @@ def test_release_threshold(self): self.app.release() # Create singleton with release_threshold=2 - self.app = Socket_Singleton(port=self.port, release_threshold=2) + self.app = SocketSingleton(port=self.port, release_threshold=2) self.received_args = [] def callback(args_tuple): @@ -700,7 +700,7 @@ def test_combined_thresholds(self): # release_threshold=5: release port after 5 clients # This tests that max_clients stops processing args, but connections continue # until release_threshold is reached - self.app = Socket_Singleton(port=self.port, max_clients=3, release_threshold=5) + self.app = SocketSingleton(port=self.port, max_clients=3, release_threshold=5) self.received_args = [] def callback(args_tuple): @@ -736,7 +736,7 @@ class TestConcurrency(unittest.TestCase): def setUp(self): """Set up singleton with observer.""" self.port = get_free_port() - self.app = Socket_Singleton(port=self.port) + self.app = SocketSingleton(port=self.port) self.received_args = [] def callback(args_tuple): From d69917a7b8854a28285d2f308d9e85bffad1113f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20C=C4=83lin?= Date: Mon, 27 Apr 2026 21:07:28 +0300 Subject: [PATCH 14/14] added a `py.typed` --- pyproject.toml | 2 +- src/socket_singleton/py.typed | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/socket_singleton/py.typed diff --git a/pyproject.toml b/pyproject.toml index 60cc118..e552e72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ license-files = ["LICEN[CS]E.*"] name = "Socket_Singleton" readme = "README.md" requires-python = ">=3.7" -version = "2.0.2" +version = "2.0.3" [project.urls] Repository = "https://github.com/emboiko/Socket_Singleton" diff --git a/src/socket_singleton/py.typed b/src/socket_singleton/py.typed new file mode 100644 index 0000000..e69de29