mirror of
https://github.com/Xevion/tcp-chat.git
synced 2025-12-06 05:16:45 -06:00
Move shared constants, exceptions & helpers.py into /shared/
This commit is contained in:
156
shared/constants.py
Normal file
156
shared/constants.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from typing import List
|
||||
from collections import namedtuple
|
||||
|
||||
import os
|
||||
import webcolors
|
||||
|
||||
__BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
HEADER_LENGTH = 10
|
||||
MINIMUM_CONTRAST = 4.65
|
||||
CLIENT_DATABASE = os.path.join(__BASE_DIR, 'client.db')
|
||||
SERVER_DATABASE = os.path.join(__BASE_DIR, 'server.db')
|
||||
|
||||
DEFAULT_IP = "127.0.0.1"
|
||||
DEFAULT_PORT = 5555
|
||||
|
||||
ConnectionOptions = namedtuple('ConnectionOptions', ['ip', 'port', 'nickname', 'password', 'remember'])
|
||||
|
||||
|
||||
class Types:
|
||||
"""
|
||||
Types describe the types of messages sent between the client and server.
|
||||
A message could be a request, a posting of information, a message etc.
|
||||
The Types class provides a universal naming for the types of messages that will be exchanged.
|
||||
"""
|
||||
REQUEST = 'REQUEST'
|
||||
MESSAGE = 'MESSAGE'
|
||||
NICKNAME = 'NICKNAME'
|
||||
USER_LIST = 'USER_LIST'
|
||||
MESSAGE_HISTORY = 'MESSAGE_HISTORY'
|
||||
|
||||
|
||||
class Requests:
|
||||
"""
|
||||
Requests describe a request between the server and client to send a specific type of information.
|
||||
The Requests class provides a universal naming for the types of requests that can be made and received.
|
||||
"""
|
||||
REQUEST_NICK = 'REQUEST_NICK' # Send the server the client's nickname
|
||||
GET_MESSAGE_HISTORY = 'GET_MESSAGE_HISTORY' # Send the client a detailed list of all messages sent up to a certain point.
|
||||
|
||||
|
||||
class Color():
|
||||
"""
|
||||
Describes a basic RGB color.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, hex: str) -> None:
|
||||
self.name, self.hex = name, hex
|
||||
self.rgb = webcolors.hex_to_rgb(hex)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'Color({self.name})'
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return '_'.join(self.name.split()).upper()
|
||||
|
||||
def relative_luminance(self) -> float:
|
||||
"""
|
||||
Calculates the relative luminance of a given color, according to WCAG guidelines.
|
||||
|
||||
https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
|
||||
:return: The relative luminance of this color.
|
||||
"""
|
||||
r, g, b = self.rgb.red / 255, self.rgb.green / 255, self.rgb.blue / 255
|
||||
r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
|
||||
g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
|
||||
b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
|
||||
def contrast_ratio(self, other: 'Color') -> float:
|
||||
"""
|
||||
Calculates the relative contrast between two colors in a ratio from 1:1 to 21:1, according to WCAG guidelines.
|
||||
|
||||
https://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
||||
|
||||
:param other: The darker color.
|
||||
:return:
|
||||
"""
|
||||
l1 = self.relative_luminance()
|
||||
l2 = other.relative_luminance()
|
||||
return (l1 + 0.05) / (l2 + 0.05)
|
||||
|
||||
|
||||
class Colors:
|
||||
"""
|
||||
Stores hexadecimal color codes of popular colors, with names.
|
||||
"""
|
||||
|
||||
AQUA = Color("Aqua", "#00ffff")
|
||||
AZURE = Color("Azure", "#f0ffff")
|
||||
BEIGE = Color("Beige", "#f5f5dc")
|
||||
BLACK = Color("Black", "#000000")
|
||||
BLUE = Color("Blue", "#0000ff")
|
||||
BROWN = Color("Brown", "#a52a2a")
|
||||
CYAN = Color("Cyan", "#00ffff")
|
||||
DARKBLUE = Color("Dark Blue", "#00008b")
|
||||
DARKCYAN = Color("Dark Cyan", "#008b8b")
|
||||
DARKGREY = Color("Dark Grey", "#a9a9a9")
|
||||
DARKGREEN = Color("Dark Green", "#006400")
|
||||
DARKKHAKI = Color("Dark Khaki", "#bdb76b")
|
||||
DARKMAGENTA = Color("Dark Magenta", "#8b008b")
|
||||
DARKOLIVEGREEN = Color("Dark Olive Green", "#556b2f")
|
||||
DARKORANGE = Color("Dark Orange", "#ff8c00")
|
||||
DARKORCHID = Color("Dark Orchid", "#9932cc")
|
||||
DARKRED = Color("Dark Red", "#8b0000")
|
||||
DARKSALMON = Color("Dark Salmon", "#e9967a")
|
||||
DARKVIOLET = Color("Dark Violet", "#9400d3")
|
||||
FUCHSIA = Color("Fuchsia", "#ff00ff")
|
||||
GOLD = Color("Gold", "#ffd700")
|
||||
GREEN = Color("Green", "#008000")
|
||||
INDIGO = Color("Indigo", "#4b0082")
|
||||
KHAKI = Color("Khaki", "#f0e68c")
|
||||
LIGHTBLUE = Color("Light Blue", "#add8e6")
|
||||
LIGHTCYAN = Color("Light Cyan", "#e0ffff")
|
||||
LIGHTGREEN = Color("Light Green", "#90ee90")
|
||||
LIGHTGREY = Color("Light Grey", "#d3d3d3")
|
||||
LIGHTPINK = Color("Light Pink", "#ffb6c1")
|
||||
LIGHTYELLOW = Color("Light Yellow", "#ffffe0")
|
||||
LIME = Color("Lime", "#00ff00")
|
||||
MAGENTA = Color("Magenta", "#ff00ff")
|
||||
MAROON = Color("Maroon", "#800000")
|
||||
NAVY = Color("Navy", "#000080")
|
||||
OLIVE = Color("Olive", "#808000")
|
||||
ORANGE = Color("Orange", "#ffa500")
|
||||
PINK = Color("Pink", "#ffc0cb")
|
||||
PURPLE = Color("Purple", "#800080")
|
||||
VIOLET = Color("Violet", "#800080")
|
||||
RED = Color("Red", "#ff0000")
|
||||
SILVER = Color("Silver", "#c0c0c0")
|
||||
WHITE = Color("White", "#ffffff")
|
||||
YELLOW = Color("Yellow", "#ffff00")
|
||||
|
||||
ALL = [AQUA, AZURE, BEIGE, BLACK, BLUE, BROWN, CYAN, DARKBLUE, DARKCYAN, DARKGREY, DARKGREEN, DARKKHAKI,
|
||||
DARKMAGENTA, DARKOLIVEGREEN, DARKORANGE, DARKORCHID, DARKRED, DARKSALMON, DARKVIOLET, FUCHSIA, GOLD, GREEN,
|
||||
INDIGO, KHAKI, LIGHTBLUE, LIGHTCYAN, LIGHTGREEN, LIGHTGREY, LIGHTPINK, LIGHTYELLOW, LIME, MAGENTA, MAROON,
|
||||
NAVY, OLIVE, ORANGE, PINK, PURPLE, VIOLET, RED, SILVER, WHITE, YELLOW]
|
||||
|
||||
@staticmethod
|
||||
def has_contrast(ratio: float, background: Color = WHITE) -> List[Color]:
|
||||
"""
|
||||
Returns a list of Color objects with contrast ratios above the given minimum.
|
||||
|
||||
:param ratio: The minimum contrast ratio each color must adhere to.
|
||||
:param background: Defaults to White, the background (lighter) for the other contrasting colors to be compared to.
|
||||
:return: A list of Color objects with contrast ratios above the given minimum.
|
||||
"""
|
||||
above_ratio = []
|
||||
|
||||
for color in Colors.ALL:
|
||||
contrast = background.contrast_ratio(color)
|
||||
if contrast >= ratio:
|
||||
above_ratio.append(color)
|
||||
|
||||
return above_ratio
|
||||
5
shared/exceptions.py
Normal file
5
shared/exceptions.py
Normal file
@@ -0,0 +1,5 @@
|
||||
class TCPChatException(BaseException):
|
||||
pass
|
||||
|
||||
class DataReceptionException(TCPChatException):
|
||||
pass
|
||||
76
shared/helpers.py
Normal file
76
shared/helpers.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import html
|
||||
import json
|
||||
import time
|
||||
from typing import List, Tuple, Any
|
||||
|
||||
from shared import constants
|
||||
|
||||
HEADER_LENGTH = 10
|
||||
|
||||
|
||||
def prepare(message: str, encoding='utf-8') -> bytes:
|
||||
"""Prepares a message for sending through a socket by adding a proper header and encoding it."""
|
||||
header = f'{len(message):<{HEADER_LENGTH}}'
|
||||
return (header + message).encode(encoding)
|
||||
|
||||
|
||||
def prepare_json(obj: Any) -> bytes:
|
||||
"""
|
||||
Prepares a object for sending as encoded JSON with a header.
|
||||
|
||||
:param obj: A JSON-encodable object
|
||||
:return: Encoded JSON
|
||||
"""
|
||||
return prepare(json.dumps(obj))
|
||||
|
||||
|
||||
def prepare_message(nickname: str, message: str, color: str, message_id: int, timestamp: int = None) -> bytes:
|
||||
return prepare_json(
|
||||
{
|
||||
'type': constants.Types.MESSAGE,
|
||||
'nickname': nickname,
|
||||
'content': message,
|
||||
'color': color,
|
||||
'time': timestamp or int(time.time()),
|
||||
'id': message_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def prepare_message_history(messages: List[Tuple[int, str, str, str, int]]) -> bytes:
|
||||
"""Returns a encoded JSON message history object with the messages provided"""
|
||||
return prepare_json(
|
||||
{
|
||||
'type': constants.Types.MESSAGE_HISTORY,
|
||||
'messages': [
|
||||
{
|
||||
'type': constants.Types.MESSAGE,
|
||||
'nickname': nickname,
|
||||
'content': message,
|
||||
'color': color,
|
||||
'time': timestamp,
|
||||
'id': message_id
|
||||
} for message_id, nickname, color, message, timestamp in messages
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def prepare_request(request: str) -> bytes:
|
||||
"""Helper function for creating a request message."""
|
||||
return prepare_json({'type': constants.Types.REQUEST, 'request': request})
|
||||
|
||||
|
||||
def formatted_message(message: dict) -> str:
|
||||
"""Given a message dict object, return a color formatted and GUI ready string."""
|
||||
nick_esc = html.escape(message["nickname"])
|
||||
msg_esc = html.escape(message["message"])
|
||||
return f'<<span style="color: {message["color"]}">{nick_esc}</span>> {msg_esc}'
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix='B'):
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.0f%s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||||
Reference in New Issue
Block a user