Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/utcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

logger = logging.getLogger("utcp")

if not logger.handlers: # Only add default handler if user didn't configure logging
if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
logger.addHandler(handler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ async def register_manual(self, manual_call_template: CallTemplate) -> RegisterM
raise ValueError(f"No registered communication protocol of type {manual_call_template.call_template_type} found, available types: {CommunicationProtocol.communication_protocols.keys()}")

result = await CommunicationProtocol.communication_protocols[manual_call_template.call_template_type].register_manual(self, manual_call_template)

if result.success:
for tool in result.manual.tools:
if not tool.name.startswith(manual_call_template.name + "."):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import json
import os
import shlex
import sys
from typing import Dict, Any, List, Optional, Callable, AsyncGenerator

from utcp.interfaces.communication_protocol import CommunicationProtocol
Expand All @@ -35,6 +36,12 @@

logger = logging.getLogger(__name__)

if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
logger.addHandler(handler)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a handler without disabling propagation can cause duplicate logs if global handlers are configured later; set propagate=False when attaching the default handler.

(Based on your team's feedback about restoring plugin logs by default, this ensures logs show once without duplication when apps later configure logging.)

Prompt for AI agents
Address the following comment on plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py at line 42:

<comment>Adding a handler without disabling propagation can cause duplicate logs if global handlers are configured later; set propagate=False when attaching the default handler.

(Based on your team&#39;s feedback about restoring plugin logs by default, this ensures logs show once without duplication when apps later configure logging.)</comment>

<file context>
@@ -35,6 +36,12 @@
+if not logger.hasHandlers():  # Only add default handler if user didn&#39;t configure logging
+    handler = logging.StreamHandler(sys.stderr)
+    handler.setFormatter(logging.Formatter(&quot;%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s&quot;))
+    logger.addHandler(handler)
+    logger.setLevel(logging.INFO)
+
</file context>

logger.setLevel(logging.INFO)


class CliCommunicationProtocol(CommunicationProtocol):
"""REQUIRED
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from typing import Dict, Any, List, Optional, Callable
import aiohttp
import asyncio
Expand All @@ -12,6 +13,13 @@

logger = logging.getLogger(__name__)

if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
logger.addHandler(handler)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a handler without disabling propagation can cause duplicate log records if root/global handlers are configured later; set propagate=False after adding the default handler.

(Based on your team's feedback about plugin logs not showing by default, this keeps the default handler while preventing duplicates when applications also configure logging.)

Prompt for AI agents
Address the following comment on plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py at line 19:

<comment>Adding a handler without disabling propagation can cause duplicate log records if root/global handlers are configured later; set propagate=False after adding the default handler.

(Based on your team&#39;s feedback about plugin logs not showing by default, this keeps the default handler while preventing duplicates when applications also configure logging.)</comment>

<file context>
@@ -12,6 +13,13 @@
+if not logger.hasHandlers():  # Only add default handler if user didn&#39;t configure logging
+    handler = logging.StreamHandler(sys.stderr)
+    handler.setFormatter(logging.Formatter(&quot;%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s&quot;))
+    logger.addHandler(handler)
+    logger.setLevel(logging.INFO)
+
</file context>
Suggested change
logger.addHandler(handler)
logger.addHandler(handler); logger.propagate = False

logger.setLevel(logging.INFO)


class GraphQLClientTransport(ClientTransportInterface):
"""
Simple, robust, production-ready GraphQL transport using gql.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Request/response handling with proper error management
"""

import sys
from typing import Dict, Any, List, Optional, Callable, AsyncGenerator
import aiohttp
import json
Expand All @@ -36,6 +37,12 @@

logger = logging.getLogger(__name__)

if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
logger.addHandler(handler)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a handler without disabling propagation can cause duplicate logs when global/root handlers are configured later. Set logger.propagate = False after adding the handler.

(Based on your team's feedback about restoring plugin logs by default, this ensures logs are shown without duplication when users later configure global logging.)

Prompt for AI agents
Address the following comment on plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py at line 43:

<comment>Adding a handler without disabling propagation can cause duplicate logs when global/root handlers are configured later. Set logger.propagate = False after adding the handler.

(Based on your team&#39;s feedback about restoring plugin logs by default, this ensures logs are shown without duplication when users later configure global logging.)</comment>

<file context>
@@ -36,6 +37,12 @@
+if not logger.hasHandlers():  # Only add default handler if user didn&#39;t configure logging
+    handler = logging.StreamHandler(sys.stderr)
+    handler.setFormatter(logging.Formatter(&quot;%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s&quot;))
+    logger.addHandler(handler)
+    logger.setLevel(logging.INFO)
+
</file context>
Suggested change
logger.addHandler(handler)
logger.addHandler(handler); logger.propagate = False

logger.setLevel(logging.INFO)

class HttpCommunicationProtocol(CommunicationProtocol):
"""REQUIRED
HTTP communication protocol implementation for UTCP client.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from typing import Dict, Any, List, Optional, Callable, AsyncIterator, AsyncGenerator
import aiohttp
import json
Expand All @@ -21,6 +22,12 @@

logger = logging.getLogger(__name__)

if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
logger.addHandler(handler)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a handler without disabling propagation can produce duplicate log entries if the app configures root/ancestor handlers after this module is imported; disable propagation when installing the default handler.

(Based on your team's feedback about restoring plugin logs by adding default logging handlers.)

Prompt for AI agents
Address the following comment on plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py at line 28:

<comment>Adding a handler without disabling propagation can produce duplicate log entries if the app configures root/ancestor handlers after this module is imported; disable propagation when installing the default handler.

(Based on your team&#39;s feedback about restoring plugin logs by adding default logging handlers.)</comment>

<file context>
@@ -21,6 +22,12 @@
+if not logger.hasHandlers():  # Only add default handler if user didn&#39;t configure logging
+    handler = logging.StreamHandler(sys.stderr)
+    handler.setFormatter(logging.Formatter(&quot;%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s&quot;))
+    logger.addHandler(handler)
+    logger.setLevel(logging.INFO)
+
</file context>

logger.setLevel(logging.INFO)

class SseCommunicationProtocol(CommunicationProtocol):
"""REQUIRED
SSE communication protocol implementation for UTCP client.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from typing import Dict, Any, List, Optional, Callable, AsyncIterator, Tuple, AsyncGenerator
import aiohttp
import json
Expand All @@ -18,6 +19,12 @@

logger = logging.getLogger(__name__)

if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.INFO)

class StreamableHttpCommunicationProtocol(CommunicationProtocol):
"""REQUIRED
Streamable HTTP communication protocol implementation for UTCP client.
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ async def test_http_register_manual_discovers_tools(
assert len(register_result.manual.tools) == 4

# Find the echo tool
echo_tool = next((tool for tool in register_result.manual.tools if tool.name == "echo"), None)
echo_tool = next((tool for tool in register_result.manual.tools if tool.name == f"{HTTP_SERVER_NAME}.echo"), None)
assert echo_tool is not None
assert "echoes back its input" in echo_tool.description

# Check for other tools
tool_names = [tool.name for tool in register_result.manual.tools]
assert "greet" in tool_names
assert "list_items" in tool_names
assert "add_numbers" in tool_names
assert f"{HTTP_SERVER_NAME}.greet" in tool_names
assert f"{HTTP_SERVER_NAME}.list_items" in tool_names
assert f"{HTTP_SERVER_NAME}.add_numbers" in tool_names


@pytest.mark.asyncio
Expand All @@ -120,7 +120,7 @@ async def test_http_structured_output(
await transport.register_manual(None, http_mcp_provider)

# Call the echo tool and verify the result
result = await transport.call_tool(None, "echo", {"message": "http_test"}, http_mcp_provider)
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.echo", {"message": "http_test"}, http_mcp_provider)
assert result == {"reply": "you said: http_test"}


Expand All @@ -135,7 +135,7 @@ async def test_http_unstructured_output(
await transport.register_manual(None, http_mcp_provider)

# Call the greet tool and verify the result
result = await transport.call_tool(None, "greet", {"name": "Alice"}, http_mcp_provider)
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.greet", {"name": "Alice"}, http_mcp_provider)
assert result == "Hello, Alice!"


Expand All @@ -150,7 +150,7 @@ async def test_http_list_output(
await transport.register_manual(None, http_mcp_provider)

# Call the list_items tool and verify the result
result = await transport.call_tool(None, "list_items", {"count": 3}, http_mcp_provider)
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.list_items", {"count": 3}, http_mcp_provider)

assert isinstance(result, list)
assert len(result) == 3
Expand All @@ -170,7 +170,7 @@ async def test_http_numeric_output(
await transport.register_manual(None, http_mcp_provider)

# Call the add_numbers tool and verify the result
result = await transport.call_tool(None, "add_numbers", {"a": 5, "b": 7}, http_mcp_provider)
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.add_numbers", {"a": 5, "b": 7}, http_mcp_provider)

assert result == 12

Expand All @@ -191,5 +191,5 @@ async def test_http_deregister_manual(
await transport.deregister_manual(None, http_mcp_provider)

# Should still be able to call tools since we create fresh sessions
result = await transport.call_tool(None, "echo", {"message": "test"}, http_mcp_provider)
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.echo", {"message": "test"}, http_mcp_provider)
assert result == {"reply": "you said: test"}
40 changes: 20 additions & 20 deletions plugins/communication_protocols/mcp/tests/test_mcp_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,31 +55,31 @@ async def test_register_manual_discovers_tools(transport: McpCommunicationProtoc
assert len(register_result.manual.tools) == 4

# Find the echo tool
echo_tool = next((tool for tool in register_result.manual.tools if tool.name == "echo"), None)
echo_tool = next((tool for tool in register_result.manual.tools if tool.name ==f"{SERVER_NAME}.echo"), None)
assert echo_tool is not None
assert "echoes back its input" in echo_tool.description

# Check for other tools
tool_names = [tool.name for tool in register_result.manual.tools]
assert "greet" in tool_names
assert "list_items" in tool_names
assert "add_numbers" in tool_names
assert f"{SERVER_NAME}.greet" in tool_names
assert f"{SERVER_NAME}.list_items" in tool_names
assert f"{SERVER_NAME}.add_numbers" in tool_names


@pytest.mark.asyncio
async def test_call_tool_succeeds(transport: McpCommunicationProtocol, mcp_manual: McpCallTemplate):
"""Verify a successful tool call after registration."""
await transport.register_manual(None, mcp_manual)

result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual)
result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual)

assert result == {"reply": "you said: test"}


@pytest.mark.asyncio
async def test_call_tool_works_without_register(transport: McpCommunicationProtocol, mcp_manual: McpCallTemplate):
"""Verify that calling a tool works without prior registration in session-per-operation mode."""
result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual)
result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual)
assert result == {"reply": "you said: test"}


Expand All @@ -88,7 +88,7 @@ async def test_structured_output_tool(transport: McpCommunicationProtocol, mcp_m
"""Test that tools with structured output (TypedDict) work correctly."""
await transport.register_manual(None, mcp_manual)

result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual)
result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual)
assert result == {"reply": "you said: test"}


Expand All @@ -97,7 +97,7 @@ async def test_unstructured_string_output(transport: McpCommunicationProtocol, m
"""Test that tools returning plain strings work correctly."""
await transport.register_manual(None, mcp_manual)

result = await transport.call_tool(None, "greet", {"name": "Alice"}, mcp_manual)
result = await transport.call_tool(None, f"{SERVER_NAME}.greet", {"name": "Alice"}, mcp_manual)
assert result == "Hello, Alice!"


Expand All @@ -106,7 +106,7 @@ async def test_list_output(transport: McpCommunicationProtocol, mcp_manual: McpC
"""Test that tools returning lists work correctly."""
await transport.register_manual(None, mcp_manual)

result = await transport.call_tool(None, "list_items", {"count": 3}, mcp_manual)
result = await transport.call_tool(None, f"{SERVER_NAME}.list_items", {"count": 3}, mcp_manual)

assert isinstance(result, list)
assert len(result) == 3
Expand All @@ -118,7 +118,7 @@ async def test_numeric_output(transport: McpCommunicationProtocol, mcp_manual: M
"""Test that tools returning numeric values work correctly."""
await transport.register_manual(None, mcp_manual)

result = await transport.call_tool(None, "add_numbers", {"a": 5, "b": 7}, mcp_manual)
result = await transport.call_tool(None, f"{SERVER_NAME}.add_numbers", {"a": 5, "b": 7}, mcp_manual)

assert result == 12

Expand All @@ -132,7 +132,7 @@ async def test_deregister_manual(transport: McpCommunicationProtocol, mcp_manual

await transport.deregister_manual(None, mcp_manual)

result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual)
result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual)
assert result == {"reply": "you said: test"}


Expand All @@ -145,7 +145,7 @@ async def test_register_resources_as_tools_disabled(transport: McpCommunicationP

# Check that no resource tools are present
tool_names = [tool.name for tool in register_result.manual.tools]
resource_tools = [name for name in tool_names if name.startswith("resource_")]
resource_tools = [name for name in tool_names if name.startswith(f"{SERVER_NAME}.resource_")]
assert len(resource_tools) == 0


Expand All @@ -160,13 +160,13 @@ async def test_register_resources_as_tools_enabled(transport: McpCommunicationPr

# Check that resource tools are present
tool_names = [tool.name for tool in register_result.manual.tools]
resource_tools = [name for name in tool_names if name.startswith("resource_")]
resource_tools = [name for name in tool_names if name.startswith(f"{SERVER_NAME}.resource_")]
assert len(resource_tools) == 2
assert "resource_get_test_document" in resource_tools
assert "resource_get_config" in resource_tools
assert f"{SERVER_NAME}.resource_get_test_document" in resource_tools
assert f"{SERVER_NAME}.resource_get_config" in resource_tools

# Check resource tool properties
test_doc_tool = next((tool for tool in register_result.manual.tools if tool.name == "resource_get_test_document"), None)
test_doc_tool = next((tool for tool in register_result.manual.tools if tool.name == f"{SERVER_NAME}.resource_get_test_document"), None)
assert test_doc_tool is not None
assert "Read resource:" in test_doc_tool.description
assert "file://test_document.txt" in test_doc_tool.description
Expand All @@ -179,7 +179,7 @@ async def test_call_resource_tool(transport: McpCommunicationProtocol, mcp_manua
await transport.register_manual(None, mcp_manual_with_resources)

# Call the test document resource
result = await transport.call_tool(None, "resource_get_test_document", {}, mcp_manual_with_resources)
result = await transport.call_tool(None, f"{SERVER_NAME}.resource_get_test_document", {}, mcp_manual_with_resources)

# Check that we get the resource content
assert isinstance(result, dict)
Expand Down Expand Up @@ -207,7 +207,7 @@ async def test_call_resource_tool_json_content(transport: McpCommunicationProtoc
await transport.register_manual(None, mcp_manual_with_resources)

# Call the config.json resource
result = await transport.call_tool(None, "resource_get_config", {}, mcp_manual_with_resources)
result = await transport.call_tool(None, f"{SERVER_NAME}.resource_get_config", {}, mcp_manual_with_resources)

# Check that we get the resource content
assert isinstance(result, dict)
Expand All @@ -232,14 +232,14 @@ async def test_call_resource_tool_json_content(transport: McpCommunicationProtoc
async def test_call_nonexistent_resource_tool(transport: McpCommunicationProtocol, mcp_manual_with_resources: McpCallTemplate):
"""Verify that calling a non-existent resource tool raises an error."""
with pytest.raises(ValueError, match="Resource 'nonexistent' not found in any configured server"):
await transport.call_tool(None, "resource_nonexistent", {}, mcp_manual_with_resources)
await transport.call_tool(None, f"{SERVER_NAME}.resource_nonexistent", {}, mcp_manual_with_resources)


@pytest.mark.asyncio
async def test_resource_tool_without_registration(transport: McpCommunicationProtocol, mcp_manual_with_resources: McpCallTemplate):
"""Verify that resource tools work even without prior registration."""
# Don't register the manual first - test direct call
result = await transport.call_tool(None, "resource_get_test_document", {}, mcp_manual_with_resources)
result = await transport.call_tool(None, f"{SERVER_NAME}.resource_get_test_document", {}, mcp_manual_with_resources)

# Should still work and return content
assert isinstance(result, dict)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import socket
import struct
import sys
from typing import Dict, Any, List, Optional, Callable, Union

from utcp.client.client_transport_interface import ClientTransportInterface
Expand All @@ -16,6 +17,12 @@

logger = logging.getLogger(__name__)

if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
logger.addHandler(handler)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a handler without disabling propagation can cause duplicate logs when global handlers are configured later; set logger.propagate = False when installing the default handler.

(Based on your team's feedback about restoring plugin logs by default; this keeps logs visible without duplicating them when global logging is configured.)

Prompt for AI agents
Address the following comment on plugins/communication_protocols/socket/src/utcp_socket/tcp_communication_protocol.py at line 23:

<comment>Adding a handler without disabling propagation can cause duplicate logs when global handlers are configured later; set logger.propagate = False when installing the default handler.

(Based on your team&#39;s feedback about restoring plugin logs by default; this keeps logs visible without duplicating them when global logging is configured.)</comment>

<file context>
@@ -16,6 +17,12 @@
+if not logger.hasHandlers():  # Only add default handler if user didn&#39;t configure logging
+    handler = logging.StreamHandler(sys.stderr)
+    handler.setFormatter(logging.Formatter(&quot;%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s&quot;))
+    logger.addHandler(handler)
+    logger.setLevel(logging.INFO)
+
</file context>

logger.setLevel(logging.INFO)

class TCPTransport(ClientTransportInterface):
"""Transport implementation for TCP-based tool providers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
tools. It does not maintain any persistent connections.
"""
import json
import sys
import yaml
import aiofiles
from pathlib import Path
Expand All @@ -25,6 +26,13 @@

logger = logging.getLogger(__name__)

if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
logger.addHandler(handler)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a handler at import time without disabling propagation can cause duplicate logs if the application configures handlers later; set logger.propagate = False when attaching the default handler.

(Based on your team's feedback about restoring plugin logs by default, this change keeps logs visible while preventing duplicates after later logging configuration.)

Prompt for AI agents
Address the following comment on plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py at line 32:

<comment>Adding a handler at import time without disabling propagation can cause duplicate logs if the application configures handlers later; set logger.propagate = False when attaching the default handler.

(Based on your team&#39;s feedback about restoring plugin logs by default, this change keeps logs visible while preventing duplicates after later logging configuration.)</comment>

<file context>
@@ -25,6 +26,13 @@
+if not logger.hasHandlers():  # Only add default handler if user didn&#39;t configure logging
+    handler = logging.StreamHandler(sys.stderr)
+    handler.setFormatter(logging.Formatter(&quot;%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s&quot;))
+    logger.addHandler(handler)
+    logger.setLevel(logging.INFO)
+
</file context>
Suggested change
logger.addHandler(handler)
logger.addHandler(handler); logger.propagate = False

logger.setLevel(logging.INFO)


class TextCommunicationProtocol(CommunicationProtocol):
"""REQUIRED
Communication protocol for file-based UTCP manuals and tools."""
Expand Down
Loading