commit 2e0bf0cb67cc024ba8c1c52dd60e4768dc480d1a Author: Michael Pilosov Date: Sun Mar 19 13:52:46 2023 -0600 adding (nonfunctional) tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e54937 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +*.eggs/ +dist/ +build/ +Pipfile* diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..68e9e6a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,39 @@ +[metadata] +name = announce_server +author = Mathematical Michael +author_email = mm@clfx.cc +url = https://git.mlden.com/mm/announce-server.git +license = MIT +description = Announces a server to a host +long_description = file: README.md +long_description_content_type = text/markdown +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + + +[options] +package_dir = + = src +packages = find: +install_requires = + python-socketio[asyncio_client] + +[options.packages.find] +where = src + +[options.extras_require] +dev = + build + setuptools_scm + pytest + pytest-mock + pytest-asyncio + asynctest; python_version<'3.8' \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..251ac8b --- /dev/null +++ b/setup.py @@ -0,0 +1,11 @@ +from setuptools import find_packages, setup + +setup( + name="announce-server", + packages=find_packages(), + setup_requires=[ + "setuptools_scm", + ], + use_scm_version=True, + python_requires=">=3.6", +) diff --git a/src/announce_server/__init__.py b/src/announce_server/__init__.py new file mode 100644 index 0000000..7d8a71d --- /dev/null +++ b/src/announce_server/__init__.py @@ -0,0 +1,2 @@ +from .decorator import _announce_server, announce_server +from .get_ip import get_ip_address diff --git a/src/announce_server/decorator.py b/src/announce_server/decorator.py new file mode 100644 index 0000000..24b08d7 --- /dev/null +++ b/src/announce_server/decorator.py @@ -0,0 +1,69 @@ +import asyncio +from functools import wraps + +import socketio + +sio = socketio.AsyncClient() + + +async def _announce_server(**kwargs): + SERVER_NAME = kwargs.get("name", "server_1") + SERVER_IP = kwargs.get("ip", "localhost") + SERVER_PORT = kwargs.get("port", 8000) + HOST_SERVER_IP = kwargs.get("host_ip", "0.0.0.0") + HOST_SERVER_PORT = kwargs.get("host_port", 5000) + + @sio.event + async def connect(): + await sio.emit( + "register", {"name": SERVER_NAME, "ip": SERVER_IP, "port": SERVER_PORT} + ) + print("Announced server to host") + + async def main(): + # retry until we connect to the host + while True: + try: + await sio.connect(f"http://{HOST_SERVER_IP}:{HOST_SERVER_PORT}") + break + except Exception as e: + print(e) + print("Failed to connect to host, retrying in 5 seconds") + await asyncio.sleep(5) + # await sio.connect(f'http://{HOST_SERVER_IP}:{HOST_SERVER_PORT}') + print("Connected to host") + + @sio.on("heartbeat") + async def on_heartbeat(): + print("Received heartbeat from host") + + @sio.event + async def disconnect(): + print("Disconnected from host") + + await main() + + +def announce_server(task=None, loop=None, **outer_kwargs): + if task is None: + return lambda f: announce_server(f, loop=loop, **outer_kwargs) + + @wraps(task) + def wrapper(*args, **kwargs): + async def main(*args, **kwargs): + if loop is not None: + host_block_thread = loop.run_in_executor(None, task) + else: + host_block_thread = asyncio.to_thread(task) + + # Announce the server to the host + await _announce_server(**outer_kwargs) + + # Wait for host_block to finish + await host_block_thread + + if loop is not None: + return loop.create_task(main(*args, **kwargs)) + else: + return asyncio.run(main(*args, **kwargs)) + return wrapper diff --git a/src/announce_server/get_ip.py b/src/announce_server/get_ip.py new file mode 100644 index 0000000..1b05ee5 --- /dev/null +++ b/src/announce_server/get_ip.py @@ -0,0 +1,14 @@ +import socket + + +def get_ip_address(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # This IP address doesn't need to be reachable, as we're only using it to find the local IP address + s.connect(("10.255.255.255", 1)) + ip = s.getsockname()[0] + except Exception: + ip = "127.0.0.1" + finally: + s.close() + return ip diff --git a/tests/test_announce.py b/tests/test_announce.py new file mode 100644 index 0000000..dd68abe --- /dev/null +++ b/tests/test_announce.py @@ -0,0 +1,51 @@ +import asyncio +import sys +from unittest.mock import patch + +if sys.version_info >= (3, 8): + from unittest.mock import AsyncMock +else: + from asynctest import CoroutineMock as AsyncMock + +import pytest + +from announce_server import _announce_server, announce_server + + +@pytest.mark.asyncio +async def test_announce_server_decorator(event_loop, mocker): + # Mock the _announce_server function to prevent actual connections + mocker.patch("announce_server._announce_server") + + # Sample function to be decorated + async def sample_async_function(): + await asyncio.sleep(1) + return "Hello, world!" + + # Decorate the sample function with announce_server + decorated_function = announce_server( + name="test_server", + ip="127.0.0.1", + port=8000, + host_ip="127.0.0.1", + host_port=5000, + loop=event_loop, # Pass the current event loop + )(sample_async_function) + + # Run the decorated function + task = await decorated_function() + await asyncio.sleep(1.1) # Sleep slightly longer than sample_async_function + task.cancel() # Cancel the task + + # Check if the _announce_server function was called with the correct arguments + announce_server._announce_server.assert_called_once_with( + name="test_server", + ip="127.0.0.1", + port=8000, + host_ip="127.0.0.1", + host_port=5000, + ) + + # Check if the decorated function returns the expected result + result = await sample_async_function() + assert result == "Hello, world!" \ No newline at end of file diff --git a/tests/test_get_ip.py b/tests/test_get_ip.py new file mode 100644 index 0000000..998f1db --- /dev/null +++ b/tests/test_get_ip.py @@ -0,0 +1,34 @@ +import socket +from unittest.mock import MagicMock, patch + +import pytest + +from announce_server import get_ip_address + + +def test_get_ip_address(): + with patch("socket.socket") as mock_socket: + # Create a MagicMock object for the socket object + mock_socket_instance = MagicMock() + mock_socket.return_value = mock_socket_instance + + # Define the expected IP address + expected_ip = "192.168.1.100" + + # Configure the mock socket instance to return the expected IP address + mock_socket_instance.getsockname.return_value = (expected_ip, 0) + + # Test the get_ip_address function + result_ip = get_ip_address() + + # Check if the result matches the expected IP address + assert result_ip == expected_ip + + # Check if the socket object was created with the correct arguments + mock_socket.assert_called_once_with(socket.AF_INET, socket.SOCK_DGRAM) + + # Check if the socket.connect method was called with the correct arguments + mock_socket_instance.connect.assert_called_once_with(("10.255.255.255", 1)) + + # Check if the socket.close method was called + mock_socket_instance.close.assert_called_once()