Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client #1

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Empty file added client/__init__.py
Empty file.
20 changes: 20 additions & 0 deletions client/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"network": {
"host": "127.0.0.1",
"port": 65432,
"receive_buffer_size": 1024
},
"blocked_domains": {
"example.com": true,
"ads.example.com": true,
"fxp.co.il": true
},
"settings": {
"ad_block": "off",
"adult_block": "off"
},
"logging": {
"level": "INFO",
"log_dir": "client_logs"
}
}
8 changes: 8 additions & 0 deletions client/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from src.Application import Application

def main() -> None:
application: Application = Application()
application.run()

if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions client/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
pythonpath = .
8 changes: 8 additions & 0 deletions client/requirments.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-e git+https://github.com/pazMenachem/My_Internet.git@b0c04b626f09baa0dace19fb70902cc8189f7ce0#egg=client&subdirectory=client
colorama==0.4.6
exceptiongroup==1.2.2
iniconfig==2.0.0
packaging==24.1
pluggy==1.5.0
pytest==8.3.3
tomli==2.0.2
7 changes: 7 additions & 0 deletions client/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from setuptools import setup, find_packages

setup(
name="client",
packages=find_packages(),
version="0.1",
)
116 changes: 116 additions & 0 deletions client/src/Application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import json
import threading
from .Communicator import Communicator
from .View import Viewer
from .Logger import setup_logger
from .ConfigManager import ConfigManager

from .utils import (
STR_CODE, STR_CONTENT,
Codes
)

class Application:
"""
Main application class that coordinates communication between UI and server.

Uses threading to handle simultaneous GUI and network operations.

Attributes:
_logger: Logger instance for application logging
_view: Viewer instance for GUI operations
_communicator: Communicator instance for network operations
"""

def __init__(self) -> None:
"""Initialize application components."""
self._logger = setup_logger(__name__)
self._config_manager = ConfigManager()
self._request_lock = threading.Lock()

self._view = Viewer(config_manager=self._config_manager, message_callback=self._handle_request)
self._communicator = Communicator(config_manager=self._config_manager, message_callback=self._handle_request)

def run(self) -> None:
"""
Start the application with threaded communication handling.

Raises:
Exception: If there's an error during startup of either component.
"""
self._logger.info("Starting application")

try:
self._start_communication()
self._start_gui()

except Exception as e:
self._logger.error(f"Error during execution: {str(e)}", exc_info=True)
raise
finally:
self._cleanup()

def _start_communication(self) -> None:
"""Initialize and start the communication thread."""
try:
self._communicator.connect()
threading.Thread(
target=self._communicator.receive_message,
daemon=True
).start()

self._logger.info("Communication server started successfully")
except Exception as e:
self._logger.error(f"Failed to start communication: {str(e)}")
raise

def _start_gui(self) -> None:
"""Start the GUI main loop."""
try:
self._logger.info("Starting GUI")
self._view.run()

except Exception as e:
self._logger.error(f"Failed to start GUI: {str(e)}")
raise

def _handle_request(self, request: str) -> None:
"""
Handle outgoing messages from the UI and Server.

Args:
request: received request from server or user input from UI.
"""
try:
self._logger.info(f"Processing request: {request}")
request_dict = json.loads(request)

with self._request_lock:
match request_dict[STR_CODE]:
case Codes.CODE_AD_BLOCK | \
Codes.CODE_ADULT_BLOCK | \
Codes.CODE_ADD_DOMAIN | \
Codes.CODE_REMOVE_DOMAIN:
self._communicator.send_message(json.dumps(request))
case Codes.CODE_DOMAIN_LIST_UPDATE:
self._view.update_domain_list(request_dict[STR_CONTENT])

except json.JSONDecodeError as e:
self._logger.error(f"Invalid JSON format: {str(e)}")
raise
except Exception as e:
self._logger.error(f"Error handling request: {str(e)}")
raise

def _cleanup(self) -> None:
"""Clean up resources and stop threads."""
self._logger.info("Cleaning up application resources")
try:
if self._communicator:
self._communicator.close()

if self._view and self._view.root.winfo_exists():
self._view.root.destroy()

except Exception as e:
self._logger.warning(f"Cleanup encountered an error: {str(e)}")
105 changes: 105 additions & 0 deletions client/src/Communicator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import socket
from typing import Optional, Callable
import json
from .Logger import setup_logger
from .utils import (
DEFAULT_HOST, DEFAULT_PORT, DEFAULT_BUFFER_SIZE,
ERR_SOCKET_NOT_SETUP,
STR_NETWORK
)

class Communicator:
def __init__(self, config_manager, message_callback: Callable[[str], None]) -> None:
"""
Initialize the communicator.

Args:
config_manager: Configuration manager instance
message_callback: Callback function to handle received messages.
"""
self.logger = setup_logger(__name__)
self.logger.info("Initializing Communicator")
self.config = config_manager.get_config()
self._message_callback = message_callback

self._host = self.config[STR_NETWORK][DEFAULT_HOST]
self._port = self.config[STR_NETWORK][DEFAULT_PORT]
self._receive_buffer_size = self.config[STR_NETWORK][DEFAULT_BUFFER_SIZE]
self._socket: Optional[socket.socket] = None

def connect(self) -> None:
"""
Establish connection to the server.

Raises:
socket.error: If connection cannot be established.
"""
try:
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.connect((self._host, self._port))
self.logger.info(f"Connected to server at {self._host}:{self._port}")
except socket.error as e:
self.logger.error(f"Failed to connect to server: {str(e)}")
raise

def send_message(self, message: str) -> None:
"""
Send a json message to the server.

Args:
message_json: The message to send to the server.

Raises:
RuntimeError: If socket connection is not established.
"""
self._validate_connection()

try:
self._socket.send(message.encode('utf-8'))
self.logger.info(f"Message sent: {message}")
except Exception as e:
self.logger.error(f"Failed to send message: {str(e)}")
raise

def receive_message(self) -> None:
"""Continuously receive and process messages from the socket connection.

This method runs in a loop to receive messages from the socket. Each received
message is decoded from UTF-8 and passed to the message callback function.

Raises:
RuntimeError: If socket connection is not established.
socket.error: If there's an error receiving data from the socket.
UnicodeDecodeError: If received data cannot be decoded as UTF-8.
"""
self._validate_connection()

self.logger.info("Starting message receive loop")
try:
while message_bytes := self._socket.recv(self._receive_buffer_size):
if not message_bytes:
self.logger.warning("Received empty message, breaking receive loop")
break
message = message_bytes.decode('utf-8')
self.logger.info(f"Received message: {message}")
self._message_callback(message)
except Exception as e:
self.logger.error(f"Error receiving message: {str(e)}")
raise

def close(self) -> None:
"""Close the socket connection and clean up resources."""
if self._socket:
try:
self._socket.close()
self.logger.info("Socket connection closed")
except Exception as e:
self.logger.error(f"Error closing socket: {str(e)}")
finally:
self._socket = None

def _validate_connection(self) -> None:
"""Validate the socket connection."""
if not self._socket:
self.logger.error(ERR_SOCKET_NOT_SETUP)
raise RuntimeError(ERR_SOCKET_NOT_SETUP)
87 changes: 87 additions & 0 deletions client/src/ConfigManager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Configuration management module for the application."""

import json
import os
from typing import Dict, Any
from .Logger import setup_logger
from .utils import DEFAULT_CONFIG


class ConfigManager:
"""Manages application configuration loading and saving."""

def __init__(self, config_file: str = "config.json") -> None:
"""
Initialize the configuration manager.

Args:
config_file: Path to the configuration file.
"""
self.logger = setup_logger(__name__)
self.config_file = config_file
self.config = self._load_config()

def _load_config(self) -> Dict[str, Any]:
"""
Load configuration from JSON file.

Returns:
Dict containing configuration settings.
"""
try:
if os.path.exists(self.config_file):
self.logger.info(f"Loading configuration from {self.config_file}")
with open(self.config_file, 'r') as f:
user_config = json.load(f)
return self._merge_configs(DEFAULT_CONFIG, user_config)

self.logger.warning(f"Configuration file not found, using default configuration")

except json.JSONDecodeError:
self.logger.error(f"Error decoding {self.config_file}, using default configuration")

return DEFAULT_CONFIG.copy()

def _merge_configs(self, default: Dict[str, Any], user: Dict[str, Any]) -> Dict[str, Any]:
"""
Recursively merge user configuration with default configuration.

Args:
default: Default configuration dictionary
user: User configuration dictionary

Returns:
Merged configuration dictionary
"""
result = default.copy()

for key, value in user.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = self._merge_configs(result[key], value)
else:
result[key] = value

return result

def save_config(self, config: Dict[str, Any]) -> None:
"""
Save configuration to JSON file.

Args:
config: Configuration dictionary to save
"""
try:
with open(self.config_file, 'w') as f:
json.dump(config, f, indent=4)
self.logger.info("Configuration saved successfully")
except Exception as e:
self.logger.error(f"Error saving configuration: {str(e)}")

def get_config(self) -> Dict[str, Any]:
"""
Get the current configuration.

Returns:
Current configuration dictionary
"""
return self.config
Loading