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

Server client #3

Open
wants to merge 41 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
93bd6e2
Add Client code files
pazMenachem Nov 1, 2024
94c2d2b
Add logger
pazMenachem Nov 3, 2024
e57f88c
Add tests
pazMenachem Nov 3, 2024
b67a51a
Modify Controller name to Application
pazMenachem Nov 4, 2024
b6f1ef1
Modify Code
pazMenachem Nov 4, 2024
bac6fd9
Modify tests
pazMenachem Nov 4, 2024
953460c
server files
yoaz11 Nov 4, 2024
294e5bc
add Factory handler class in handlers file
yoaz11 Nov 4, 2024
e5c58db
change the adult content block logic, and server route kernal request…
yoaz11 Nov 4, 2024
f1ebd70
Add config file and ConfigManager
pazMenachem Nov 5, 2024
c4a2a4a
Add config manager to files
pazMenachem Nov 5, 2024
1dce509
Add Update domain list func and Thread safety messures
pazMenachem Nov 5, 2024
298ed61
fix RequestFactory class - send to Adult Content class the db manager
yoaz11 Nov 5, 2024
5ecfb93
add tests for the handlers class
yoaz11 Nov 5, 2024
24b5695
add test for handlers
yoaz11 Nov 5, 2024
7822b04
tests for the server
yoaz11 Nov 5, 2024
42169ce
change imports in handlers,server,main
yoaz11 Nov 5, 2024
f82015f
Add utlis and consts
pazMenachem Nov 6, 2024
957850b
Finish without tests
pazMenachem Nov 6, 2024
b0c04b6
Finish tests
pazMenachem Nov 6, 2024
22bdea4
Update requirments
pazMenachem Nov 6, 2024
1ea52e8
Restructure response code system to match client
yoaz11 Nov 6, 2024
367cb99
Add state management system and enhance database operations
yoaz11 Nov 6, 2024
ffc8a3c
update handlers with new state management
yoaz11 Nov 6, 2024
c2e761b
Update server with comprehensive connection handling and state manage…
yoaz11 Nov 6, 2024
b93cd74
change logic - start ingreate with client logic
yoaz11 Nov 6, 2024
a2e3788
remove state manager
yoaz11 Nov 6, 2024
5f1dfbf
server files
yoaz11 Nov 10, 2024
cc9c70b
changed logic , add tests , loger , utils
yoaz11 Nov 10, 2024
396ebe7
Merged with updated server files
pazMenachem Nov 11, 2024
e496893
Merge branch 'server' of https://github.com/pazMenachem/My_Internet i…
pazMenachem Nov 11, 2024
1ba71b0
Modify code
pazMenachem Nov 11, 2024
e50477e
Merge branch 'client' into server
pazMenachem Nov 11, 2024
036d447
Finish integration
pazMenachem Nov 12, 2024
fce11b8
change handlers response
yoaz11 Nov 12, 2024
886ccd4
add method to send init settings
yoaz11 Nov 12, 2024
8e3b20b
Modify client code.
pazMenachem Nov 12, 2024
21646bd
Merge branch 'server_client' of https://github.com/pazMenachem/My_Int…
yoaz11 Nov 12, 2024
e956888
Modify code, content in response msg
pazMenachem Nov 12, 2024
4609155
Bug fix - duplicates domains
yoaz11 Nov 12, 2024
466fe6c
Modify tests
pazMenachem Nov 14, 2024
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -845,3 +845,5 @@ FodyWeavers.xsd
# Additional files built by Visual Studio

# End of https://www.toptal.com/developers/gitignore/api/python,c,windows,linux,visualstudio,pycharm,clion
server/client.py
server/my_internet.db
Empty file added __init__.py
Empty file.
Empty file added client/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions client/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"network": {
"host": "127.0.0.1",
"port": 65432,
"receive_buffer_size": 1024
},
"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",
)
124 changes: 124 additions & 0 deletions client/src/Application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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_DOMAINS, STR_OPERATION,
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._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, to_server: bool = True) -> 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)

if to_server:
message = request if isinstance(request, dict) else json.loads(request)
self._communicator.send_message(message)
return

match request_dict[STR_OPERATION]:
case Codes.CODE_INIT_SETTINGS:
self._view.update_initial_settings(request_dict)
case Codes.CODE_AD_BLOCK:
self._view.ad_block_response(request_dict)
case Codes.CODE_ADULT_BLOCK:
self._view.adult_block_response(request_dict)
case Codes.CODE_ADD_DOMAIN:
self._view.add_domain_response(request_dict)
case Codes.CODE_REMOVE_DOMAIN:
self._view.remove_domain_response(request_dict)
case Codes.CODE_DOMAIN_LIST_UPDATE:
self._view.update_domain_list_response(request_dict[STR_DOMAINS])

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)}")
104 changes: 104 additions & 0 deletions client/src/Communicator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import socket
from typing import Optional, Callable
import json
from .Logger import setup_logger
from .utils import (
ERR_SOCKET_NOT_SETUP,
STR_NETWORK, STR_HOST, STR_PORT, STR_RECEIVE_BUFFER_SIZE
)

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][STR_HOST]
self._port = int(self.config[STR_NETWORK][STR_PORT])
self._receive_buffer_size = int(self.config[STR_NETWORK][STR_RECEIVE_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, request: dict) -> None:
"""
Send a json request to the server.

Args:
request: The request to send to the server.

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

try:
self._socket.send(json.dumps(request).encode('utf-8'))
self.logger.info(f"Request sent: {request}")
except Exception as e:
self.logger.error(f"Failed to send request: {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, False)
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