From b9aef3fee3886193da25d27777650e16670a4bc3 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 20 Feb 2025 22:54:53 +0100 Subject: [PATCH] Final sin video --- Controlador.py | 56 ++++++--- Modelo.py | 16 ++- README.md | 38 ++++++ Vista.py | 149 ++++++++++++++++++++---- __pycache__/Controlador.cpython-313.pyc | Bin 0 -> 4024 bytes __pycache__/Modelo.cpython-313.pyc | Bin 0 -> 3375 bytes __pycache__/Vista.cpython-313.pyc | Bin 0 -> 9352 bytes __pycache__/server.cpython-313.pyc | Bin 0 -> 2375 bytes 8 files changed, 210 insertions(+), 49 deletions(-) create mode 100644 __pycache__/Controlador.cpython-313.pyc create mode 100644 __pycache__/Modelo.cpython-313.pyc create mode 100644 __pycache__/Vista.cpython-313.pyc create mode 100644 __pycache__/server.cpython-313.pyc diff --git a/Controlador.py b/Controlador.py index 811bc95..0fbbe6d 100644 --- a/Controlador.py +++ b/Controlador.py @@ -5,29 +5,47 @@ from server import start_server class ControladorChat: def __init__(self, vista): self.vista = vista - self.modelo = ModeloCliente() + self.modelos = {} def iniciar_servidor(self): """Inicia el servidor en un hilo separado.""" threading.Thread(target=start_server, daemon=True).start() - self.vista.mostrar_mensaje("[SERVIDOR] Servidor iniciado en segundo plano...\n") + #vista.mostrar_mensaje("[SERVIDOR] Servidor iniciado en segundo plano...\n") + + + def enviar_mensaje(self, host): + """Envía un mensaje al servidor y lo muestra en el chat.""" + if host in self.modelos: + mensaje = self.vista.chats_frames[host]["entry"].get() + if mensaje: + self.vista.mostrar_mensaje(host, f"[TÚ] {mensaje}") + self.modelos[host].enviar_mensaje(mensaje) + self.vista.chats_frames[host]["entry"].delete(0, 'end') + + + def actualizar_host_port(self): + """Obtiene valores de host y port desde la interfaz y los actualiza en el modelo.""" + host = self.vista.host_entry.get() + port = int(self.vista.port_entry.get()) + self.modelo.set_host_port(host, port) + self.vista.mostrar_mensaje(f"[CONFIG] Host y puerto actualizados: {host}:{port}") def conectar_cliente(self): - """Intenta conectar el cliente al servidor.""" - mensaje = self.modelo.conectar( - on_message_received=self.vista.mostrar_mensaje, - on_error=self.vista.mostrar_mensaje - ) - self.vista.mostrar_mensaje(mensaje) - if self.modelo.connected: - self.vista.habilitar_envio() + """Conecta un nuevo cliente y agrega su botón en el menú.""" + host = self.vista.host_entry.get() + port = int(self.vista.port_entry.get()) - def enviar_mensaje(self): - """Obtiene el mensaje de la vista y lo envía al servidor, además lo imprime en la interfaz.""" - mensaje = self.vista.message_entry.get() - if mensaje: - self.vista.mostrar_mensaje(f"[TÚ] {mensaje}") # Agregar el mensaje a la vista - error = self.modelo.enviar_mensaje(mensaje) - if error: - self.vista.mostrar_mensaje(error) - self.vista.message_entry.delete(0, 'end') # Limpiar el campo de entrada + if host not in self.modelos: + modelo = ModeloCliente() + modelo.set_host_port(host, port) + mensaje = modelo.conectar( + on_message_received=lambda msg: self.vista.mostrar_mensaje(host, msg), + on_error=lambda err: self.vista.mostrar_mensaje(host, err) + ) + + if modelo.connected: + self.modelos[host] = modelo + self.vista.agregar_boton_chat(host) + self.vista.habilitar_envio(host) + + self.vista.mostrar_mensaje(host, mensaje) \ No newline at end of file diff --git a/Modelo.py b/Modelo.py index 1b4f496..39f699b 100644 --- a/Modelo.py +++ b/Modelo.py @@ -1,26 +1,30 @@ import socket import threading -SERVER_HOST = '127.0.0.1' -SERVER_PORT = 3333 - class ModeloCliente: def __init__(self): self.client_socket = None self.connected = False + self.host = '127.0.0.1' # Valores por defecto + self.port = 3333 + + def set_host_port(self, host, port): + """Actualiza los valores de host y port.""" + self.host = host + self.port = port def conectar(self, on_message_received, on_error): - """Conecta el cliente al servidor.""" + """Conecta el cliente al servidor con el host y puerto definidos.""" if self.connected: on_error("[CLIENTE] Ya estás conectado al servidor.\n") return self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: - self.client_socket.connect((SERVER_HOST, SERVER_PORT)) + self.client_socket.connect((self.host, self.port)) self.connected = True threading.Thread(target=self.recibir_mensajes, args=(on_message_received, on_error), daemon=True).start() - return f"[CONECTADO] Conectado al servidor {SERVER_HOST}:{SERVER_PORT}\n" + return f"[CONECTADO] Conectado al servidor {self.host}:{self.port}\n" except ConnectionRefusedError: self.client_socket = None return "[ERROR] El servidor no está disponible. Inícialo primero.\n" diff --git a/README.md b/README.md index e69de29..4e0c475 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,38 @@ +# 🗨️ Chat Cliente-Servidor en Python + +Este proyecto es una aplicación de chat basada en una arquitectura cliente-servidor utilizando Python. Implementa `socket` para la comunicación en red, `threading` para manejar múltiples clientes simultáneamente y `tkinter` para la interfaz gráfica. Permite a los usuarios conectarse a un servidor de chat y enviar mensajes en tiempo real. + +## 📌 Características + +- ✅ **Servidor multicliente** basado en `socket` y `threading`. +- ✅ **Clientes con interfaz gráfica** (Tkinter) para conectarse y chatear. +- ✅ **Interfaz amigable** con opciones de conexión y envío de mensajes. +- ✅ **Soporte para múltiples clientes** en una misma sesión de chat. +- ✅ **Servidor ejecutable en segundo plano** desde la interfaz del cliente. + + +## 🔧 Dependencias + +Este proyecto usa módulos estándar de Python, por lo que no es necesario instalar paquetes adicionales. Sin embargo, se recomienda usar un entorno virtual para mantener el aislamiento del proyecto. + +Módulos utilizados: +- [`socket`](https://docs.python.org/3/library/socket.html) - Para la comunicación en red. +- [`threading`](https://docs.python.org/3/library/threading.html) - Para manejar múltiples clientes en paralelo. +- [`tkinter`](https://docs.python.org/3/library/tkinter.html) - Para la interfaz gráfica de la aplicación. + +## 🎥 Video Tutorial + +[![Ver en YouTube](https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg)](https://youtu.be/GvhLM6T-Zhg) + + +## 🚀 Instalación y Ejecución + +### 1️⃣ Clonar el repositorio +```bash +git git clone https://git.ieslamar.org/DonWilliam/ChatPersonas.git +cd ChatPersonas + + + + + diff --git a/Vista.py b/Vista.py index 925dcff..5d72fc2 100644 --- a/Vista.py +++ b/Vista.py @@ -1,43 +1,144 @@ import tkinter as tk from tkinter import scrolledtext +from server import start_server class VistaChat: def __init__(self, root, controlador): self.root = root self.root.title("Chat Cliente-Servidor") + self.root.geometry("500x400") # Establece tamaño fijo + self.root.resizable(False, False) # Bloquea el redimensionamiento + self.root.configure(bg="#2C3E50") self.controlador = controlador - # Botón para iniciar el servidor - self.server_button = tk.Button(root, text="Iniciar Servidor", command=self.controlador.iniciar_servidor) - self.server_button.grid(row=0, column=0, padx=10, pady=10) + # Estilos de los botones + btn_style = {"font": ("Arial", 10, "bold"), "fg": "white", "bg": "#2980B9", "bd": 0, "relief": "flat"} - # Botón para conectar el cliente - self.connect_button = tk.Button(root, text="Conectar Cliente", command=self.controlador.conectar_cliente) - self.connect_button.grid(row=0, column=1, padx=10, pady=10) + # FRAME LATERAL PARA EL MENÚ + self.menu_frame = tk.Frame(root, width=100, bg="#34495E") + self.menu_frame.grid(row=0, column=0, rowspan=3, sticky="ns") - # Área de chat - self.chat_display = scrolledtext.ScrolledText(root, wrap=tk.WORD, state='disabled', width=50, height=20) - self.chat_display.grid(row=1, column=0, padx=10, pady=10, columnspan=2) + #self.button_inicio = tk.Button(self.menu_frame, text="Inicio", command=self.mostrar_inicio, width=12, **btn_style) + #self.button_inicio.pack(pady=10) - # Campo de entrada de mensajes - self.message_entry = tk.Entry(root, width=40) - self.message_entry.grid(row=2, column=0, padx=10, pady=10) + self.button_conf = tk.Button(self.menu_frame, text="Conf", command=self.mostrar_config, width=12, **btn_style) + self.button_conf.pack(pady=10) - # Botón para enviar mensajes - self.send_button = tk.Button(root, text="Enviar", command=self.controlador.enviar_mensaje, state='disabled') - self.send_button.grid(row=2, column=1, padx=10, pady=10) + # Almacenar botones de clientes conectados + self.chat_buttons = {} + self.chats_frames = {} + + # FRAME PRINCIPAL DONDE SE MUESTRA EL CHAT O LA CONFIGURACIÓN + self.main_frame = tk.Frame(root, bg="#2C3E50") + self.main_frame.grid(row=0, column=1, padx=10, pady=10, rowspan=3) - def mostrar_mensaje(self, mensaje): - """Muestra un mensaje en el chat.""" - self.chat_display.config(state='normal') - self.chat_display.insert(tk.END, mensaje + '\n') - self.chat_display.config(state='disabled') - self.chat_display.yview(tk.END) + # INICIALIZA LOS CHATS Y CONFIGURACIÓN + self.inicializar_config() + #self.inicializar_chat("127.0.0.1") # Chat local por defecto + #self.mostrar_chat("127.0.0.1") + + - def habilitar_envio(self): - """Habilita el botón de enviar mensajes.""" - self.send_button.config(state='normal') + def inicializar_chat(self, host): + """Crea un nuevo chat con un host específico.""" + chat_frame = tk.Frame(self.main_frame, bg="#2C3E50") + chat_display = scrolledtext.ScrolledText(chat_frame, wrap=tk.WORD, state='disabled', width=50, height=15, bg="#ECF0F1", fg="black") + chat_display.pack(padx=10, pady=10) + + entry_frame = tk.Frame(chat_frame, bg="#2C3E50") + entry_frame.pack(pady=5) + + message_entry = tk.Entry(entry_frame, width=40, font=("Arial", 10)) + message_entry.pack(side=tk.LEFT, padx=10, pady=5) + + send_button = tk.Button(entry_frame, text="Enviar", command=lambda: self.controlador.enviar_mensaje(host), state='disabled', font=("Arial", 10, "bold"), fg="white", bg="#27AE60", bd=0, relief="flat") + send_button.pack(side=tk.RIGHT, padx=5, pady=5) + + self.chats_frames[host] = { + "frame": chat_frame, + "display": chat_display, + "entry": message_entry, + "send_button": send_button + } + + def inicializar_config(self): + """Configura la interfaz de configuración.""" + self.config_frame = tk.Frame(self.main_frame, bg="#2C3E50") + + tk.Label(self.config_frame, text="HOST:", bg="#2C3E50", fg="white", font=("Arial", 10, "bold")).pack(pady=2) + self.host_entry = tk.Entry(self.config_frame, width=20, font=("Arial", 10)) + self.host_entry.pack(pady=5) + self.host_entry.insert(0, "127.0.0.1") + + tk.Label(self.config_frame, text="PORT:", bg="#2C3E50", fg="white", font=("Arial", 10, "bold")).pack(pady=2) + self.port_entry = tk.Entry(self.config_frame, width=10, font=("Arial", 10)) + self.port_entry.pack(pady=5) + self.port_entry.insert(0, "3333") + + btn_style = {"font": ("Arial", 10, "bold"), "fg": "white", "bg": "#2980B9", "bd": 0, "relief": "flat"} + + self.connect_button = tk.Button(self.config_frame, text="Conectar Cliente", command=self.controlador.conectar_cliente, **btn_style) + self.connect_button.pack(pady=10) + + self.start_server = tk.Button(self.config_frame, text="Abrir servidor", command=self.controlador.iniciar_servidor, **btn_style) + self.start_server.pack(pady=20) + + + + def mostrar_inicio(self): + """Muestra la pantalla de chat predeterminado (127.0.0.1).""" + self.config_frame.pack_forget() + if "127.0.0.1" in self.chats_frames: + self.chats_frames["127.0.0.1"]["frame"].pack() + + def mostrar_chat(self, host): + """Muestra un chat específico y oculta los anteriores, incluyendo la configuración.""" + # Ocultar la configuración si está visible + self.config_frame.pack_forget() + + # Ocultar todos los chats antes de mostrar el nuevo + for chat_host, chat in self.chats_frames.items(): + chat["frame"].pack_forget() + + # Mostrar el chat seleccionado + if host in self.chats_frames: + self.chats_frames[host]["frame"].pack() + + def mostrar_config(self): + """Muestra la pantalla de configuración y oculta cualquier chat abierto.""" + # Ocultar todos los chats + for chat in self.chats_frames.values(): + chat["frame"].pack_forget() + + # Mostrar la configuración + self.config_frame.pack() + + + + def mostrar_mensaje(self, host, mensaje): + """Muestra un mensaje en el chat correspondiente.""" + if host in self.chats_frames: + chat_display = self.chats_frames[host]["display"] + chat_display.config(state='normal') + chat_display.insert(tk.END, mensaje + '\n') + chat_display.config(state='disabled') + chat_display.yview(tk.END) + + def habilitar_envio(self, host): + """Habilita el botón de enviar mensajes para el chat correspondiente.""" + if host in self.chats_frames: + self.chats_frames[host]["send_button"].config(state='normal') + def deshabilitar_envio(self): """Deshabilita el botón de enviar mensajes.""" self.send_button.config(state='disabled') + + def agregar_boton_chat(self, host): + """Agrega un nuevo botón al menú lateral cuando se conecta un nuevo cliente.""" + if host not in self.chat_buttons: + btn_style = {"font": ("Arial", 10, "bold"), "fg": "white", "bg": "#2980B9", "bd": 0, "relief": "flat"} + chat_button = tk.Button(self.menu_frame, text=f"Chat {host}", command=lambda: self.mostrar_chat(host), width=12, **btn_style) + chat_button.pack(pady=5) + self.chat_buttons[host] = chat_button + self.inicializar_chat(host) diff --git a/__pycache__/Controlador.cpython-313.pyc b/__pycache__/Controlador.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..243cd19c40c7e9da97235cb4a452f9c6f35cd3a9 GIT binary patch literal 4024 zcmb_fTTC3+89p;Ry9~>~PPiagF<5xtZd@FG_E0;4!h%J=;_EnFOJ1f}y?y+e;{y@ImiKaWs(dL*JALK+h+dXmgg2G&`Yb zL$|1rQ!)mfUR11a{lF<*Qq1L!V;}sEUxsdjTo-fhGIpaRMRH{wNS54ZViDHyFKeK% z)0B6wVZsV$s=1pKEodS)NpeQh6Z0v!kU9TTby?Hr(;#|m9u@ci77bl7=W7&;-n#AZ zvaIQvCCg4v%??sT%R2!S&<5QqIq2&BMev~e)Ef6o;TTp(x}qjb@Q&`4;xO1CbI^M& zUVHJL=b>)Y$Fp!}8qIvN=?UNL8tF2x|@gxi0Yd383$4wuN3r4uDIS2>qQDR{aR z+eXINvfD|hUwOOcucu4ArDmx#OtF_rFmh=dfaSm54PUFSC4334s#DgF|003HI2?Oh zUw-r_1!0p{byHbVC8Y*Xw}!dbObza$~UQ!S53!scw3USHjg3#d>;XF9I zr=N*#B}pDC>1mYuay<~A=7$I~*P&Y_U$=xdC)X$UT23E?dpDQXmx|#LJ3O+TvBP6K zGj@2g7@o4jQ+wgJ)@Hs6g%8Be&ER^lD4w;&v)dPU#vcqlIJJ9zdSCn@ZVBtcuamae zR}|0N;`yDSeQ}~fF=UHF+ngrU5G>Md>6Sq)n3}MXQzQ-29KETm zz(&q6B_(YYl$^Fw#aJ)bem3CUq5ViDehcM!$N zt~1c@fx3Xq4eYpF| z^~d~=pM`TJmcIny;bZbKw?UE(BjW!G={#43^a&iF7t&0U1C9+q{W`d$avuU-1>6+} z19-+8fH%pdm>STt@fYAl>RO6Rk&94I^^zsNG={oriBI@mV|5?!ZLcGMX(d0s4f%;} ztZkATf|DoMrt4Xr$r|T6fvWqts7^JaO8KgG$#PjP;xAxr3(!{6mYa_Bg1T%-X|F^D zzg4nS%_@>vkQNN<(Pxm0+;jw4;nAO?Pt{(S{5#-xJPF-p79htqO2ToBvvn=Wq)1t@ zenK(H-)CJ28EGYF zUWS23a~5QoXyJdYk{#~z;HSaj=&U^oeztopxj&kE*17(__oSmBJK}>BPU#63q!-`_ z9fR&w_?cnoK-sP2A8-8bMseUxd*IE7+{5(#z^tn))SVrM4oTfKm8>dLHLYsPYR2JV z5o8!N7HRcNJBVyCdQyZeMRa==1X`D(VI00}K&Kf-2om7b?qPz;fCylUm4*{qR2H7?{Sr)q*95DU=fnp7so0qUNy5Xj(#nOcW3X+7KK4u7~Hb9 zKYT1)I((4`{iO%2)^V{&5WkOfsN&%JC@9O2)e1Qr3$pw(NT#KokSyQSsA=UiUDXX) zrueZ$5!P<0M$h30{nF`@W%$&vw6v^P7S$FCmTJoKed3CC--2Q>Z$x}Tq$lL`Ke;F~eng;iDgOgPdMjK2 literal 0 HcmV?d00001 diff --git a/__pycache__/Modelo.cpython-313.pyc b/__pycache__/Modelo.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b249da50353eb819b17e32960b6ef4f644535712 GIT binary patch literal 3375 zcmb7GO>7&-6`tiTNs(Gg8;Sa{DpM<2l1c6P6Y}u?xr$Q>a;YxrDNm#7O5xoty ztIRCbSiKl1Q0R~#K2#T9lJw90x&qCkKi6bnTS_tG~c zmw!+X9ZGNByqS6X-pqU7yrV$CkDzV-^0Ul-8$y3$1E&d9=I9JCTS!3)_a-8ho+Dg8 z8bOL{04e+xS7jw}Ww@~LH5(*S6;Xw9)xpt4V7AZ_BAkMVOW_F5G!Q}Ifw~m|s7G-F z^(r2qZHkxpGHqd>9hfuIIBU*ib!=GpYi3%?(aMxF3wZcHx(0;Vx`494Rhq)VsJ2>h z!Ki960_$PKc9vTPg@wee4c~=}6Or%0?}F|QAha#mfmHd+Dm7DYW6SoXOvAt_3#V=2 zo=GiRSTl(g=4^rD>Qgj@%78=@5_ysC068Qz*0zlum>;h4(s2!6E2;rC|T2@V@VeIg+Y* z1cTby48mXw-I3}ib4RY93*4P+V=S7oa#~j3(B!O1<#jD<5=`YZmYJLKeVO?gNrYX5 z#SLi(66T4>_?m#SkIS-5;OLMYpx9E`W|eKVzt!wr(Wli%TwBUkB<*rmCKt>tYV(tEuHLkLr(Gk=z?_@!Jx^b4- z($bu4fVisd4KXVrzgk<8x!lrVB|C$b2GWie5ezT8>Ry#2xwd4H9b362yBx0Gk1%_J z^|8a<>ALJvctx1xlYNe?-e0M*GE&^fTiJmW&+(SRjQ5gz6b+-ren+DhX%IA^jb&u* ztcFmgZE>Js4On6TPBgAjd<%S8BTrpJu;1XL{Ajgb;a1Rfeh95_VgJVTjLGs;lW|tA zq%fAXtPEMXuBS~RLmsm!C%AK%SSCdK3glwiq>xmd?x!go(o|j}`YI-7#J>@Cb|*~mR9?%$ zcd7=HMtiTy;SK`%fox9|*SyhN>TDuDZ?`MAXWmqm`D8pgM_Az6UeLgNuIIV!v+fbB zrFA1?d*;imEfJj3@9IQd#Rk=WjH&IWa2%H13of(mx@jcwN{-@moDh@PzWBQ-yvD>~ zp^PJTw`ss`O0^7DK^xZB0cG1jh|3!|Oj)P^zElCG&2g<2IUPC61o{;afR*;n52inu zJ`jU@Ju?L{Rua4Ze)80Z%a4|S^ZMiI?ddd*WL!pLdQOA(v0# zO2cEDv4R*n=uSTG>}N7Cq$(@*Oh0+&N$P3WUU%#-V(dSMZZ`Xhj)9ZUg5Tq3`q6Ix zYq55I_neQ>%VNyKe}OpQzwo$$HsL9IS;~0&PhhrC2!4>mz_+$!kmZsEjtBzlD*AbwDTtRETF8?34cw1D$umPV0K#SS}hE`4ZB2@9g zvoVom@rq?9Tp{-xGVXr)5nRnd(i2F1L(XdQx()#y(GdjUD|gOX`Sy1!AW=sUI&E0DX4C2cM6_dTu#VpE|5|1fv{V(y^e)V z!B1p17ixY}TT}29tf6Z_ApSeLAfyAmrGZd!;Cz1Id~x7He&E8+=>EXOL+@YvLZ#q| z1L@R9Z~Qv(abjm;e{`xKUHhgTb#}iz6y1T|1F3)S#2W?a`z0w@lt%K>NKrbIm(CQV zv!$`{L!lszl!nJ23cu($ka}KGRsj5LD~(7RA!lL9 z#`rP7i;Z<%BkYl>-XH`3luTtzg=*1A+a7GN`-hr}z565rgtBvT0Is8=!huwKu=%>1 zXIX_!ZvuIM{?Xp`&?@$Y;qTDJ2clfVrJc@8r$6yN?%3|w`N{s78wKg+Hy$JfUmm(S zSa{HP^A%;K$)mGT4|?WtMg2Ts36}`_S*2=ds~9R(zpAd9>0FlaqN=_Fg=2N5Q&m@V zLanTBV8c{Z!fF@73K4Re5t)%oKg?zH~l&V=4(eLIgOjUIomCL?z1xZ-MmkDDi zD`J0+UbqDJi5DUd#Q&N*&b=59xO@K<^16eSjch5LGAknpyS<8%tBhmML8UZpyH+uO hnoNNVAPdnRAcs81abKdqmq_}zaD@xLKtRgd{tK_U<3a!c literal 0 HcmV?d00001 diff --git a/__pycache__/Vista.cpython-313.pyc b/__pycache__/Vista.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..310f05beb518816fc2d0ff52a142fc4e8e73c401 GIT binary patch literal 9352 zcmcgyZ)_7umT$Ma?M~eJL&*Px#E<~-z?vikf@fyAfh0^IAu#D=mw_y%V|SdMbnH-V zhe`H!7v1Sj;WVomjf9p#BavC1G~vTZS?x)O?h~gu>Gx=CN-G^-@{I|5y1o5$ z?^So(#_7QBNOu*vs=BJH>h-Hv@AuxTyr`%sXCU49<-a7_n;7Ol@xe))U3qZ=Dz_Pl zkvt;|@j%IrFy~mp2C>aM;yuR^F32=8`xuF9W+eU?YmFp)!V~gs;=@?TV~To=q%=*5 zrt;5PV~zoJO}7gmGe#!)%iD^J_gPN@CZ50!VGz?00qIq zJ5Fr}4mAK3b-<;@2v7I7!+F0@99ot@AFWml4+BjNf6{J%n>(zM1l2YHsjE+?C&_**B3c96k=f>HJ+ZO?``5yG1;UZ z;!H0|-7tNzl$M!KntpiD7v!Yr)6;5fZpq{ZQpuT3oEvE4P+^$;;hJOb6`ZbVZvXZ{Qal z!7nVEPA8*!dP!4s)YCyo<{Pm$rzSz2>Qi&dqMDq-1&>alC|rcbR8rQbsNxGRESW+y z3hSh!QL`qm48u;F4`$Sl0#|P`8?}uem2C*($7Q$5KCZe|m2EuoMCi5~aze8qG~bCl z5%%X>S`4A(&d}|03mzY@r9<1 z);3xelX2E)KJHFL?}O8iJd})cFhyBKPG~oT_NT($7r;9vW+Nch8I)p*DAmwzwVuG2y_4qG%o#rHxl)&Ma;BU!pW z;!lEx9Itr!4f+=9=JE&&dO6?ePVFQhGo_51ev)^a zsc440F3l6i0KEaJI^un4rJ6#&Qv;pMNxLQi6Z%-GrkVLb0FD4wpthG9K+l#XeUrY| ztnY$Qu)4keBs=MyZrToo4xS3i8>K9@6f=R?lV0mKdph<3HeO! z29Xy`9xR@;LY1fKn^n}rY?|t1g2YjE8e&NZ>r`!`p+OEnw%G;QeU{*kSz4F1G;kd> zgg`m#YFsG{G<`$KMOA*;oGJf{m;RzCfddN>%MI>x5r^sZHV4;*l zM}X2{15?GkqU&-(iPA>$J`B}SOs-u+^a)!0t}f-R*}OB~5zh1$z?uKK$vmrT_{F79 zFWrgXOMRAF4;giDuarM)=*Ts6KW^w=Z~pgv|8el&4rbrGl8q_ZMT(ENA^qq7yh;B_3K##xd_gseyXkp*1Mh#UWc`pJ%cP1 zBkVOS6jsCHI=hU;&)9doP@MH%@B(=a7rjMme?EP0_Oscw_txK|H1q>{)CV^jIyUO| z0W^MF#&?vjaQ2sUJ{H=kq{Yy}aT4l$iYFV8zaC4#*4zetC4qhOgI zz93D!NnwcsREV>Hh=L9Udjz6Q1W60vksD+Lvr)__@?-Cx%6fYH!`<-Lv&>(-FbW;vp}1w5utO~OQJvwR^1;Uf!-`j8rG8C1hWl#po5a(~TAXu&2MNKG4} z2#q`;IRy11(2K?c`95USg0K;dyaf$r>kc4BiAGyM{}rfcKrwJghVKqr4#|TiqjiA# z6wie6kA;5_KB>*^+Gp(Ax7M)!{=@$4dozZR$O)PuXj!6Xv6B96bF1Y7a9gz*2{V&KSye!r);#EpB_7QCn4RoA#&SH$Rw z01mc~ve|dWvs2e{Q<^cQ<)(gOO#K9a5!#{mv<2eufUIhvei}NFIUxuTy*i!6GK7ph zd-v>WJa?eqIM5H{_6)PxlVjOSQ@KmC#--WZr6uFi63hzi(EC%*yJ#A~gXP?z_lMR& zPC!nrp3VwwkA;KV!4(Ccg6mzV+{Ta-l%>fn%-Ra1x98 zcpmHuD;MTZcoW_bx7>d|qhQPdH3NiB(y|5>)C}l6ED$BGfVP-dlMuuRcI8iUC`@Ky zQt|`H#y~ZE0Hqng07Y~rMG_DcLGzHFy!#%@oJQNjw(jy_4r=A4%*Mj8Z~`D!4>b-4 zVBk&W_XXDPH-!C9go7J_`a7Mu=0isFp|1mnHpJ?jxW^Fp+?g}PH`e+M@n}vwVTdQ5 zif=ymGU8!tlW=34gd0RRi38fc+a|eh!mAj+n;8acJp*(sbWD0B58WuFq9heI>O3G+ zfPn~WwcCcjS{x?n{Prq&q%SO0SS7(G*pN4aa_UF~xQ$c_irmKFQZNSm zaqGg$S&*Yo8g8A>iTe$4|2N{nZ@o+~@?2m*ymogQyF0%QbQVM{ZipSL?;7IaoY-TC zJx|4>C~Et+>w~fEb*C zsU0yWRSYkg306XF&^ML{pwh7R-UiJdL&Hm; ziC^TV9W3$E@by!W(H*X9%GK^SYWL@A4;!_Izo`wc47%)P-4WPJjI$NA_){ThZCLRs zMD|kMAW+=^XAc@m6Fqx6RmG)U@LYWMXmqT2e*1limAq)FLF~+BLth=CgLoPDJ(#Cp zSHmoBYw-CsTs*5+ui->Zz?SqSd?EjG*pUiQ4tCTes3e1m=0uwaXoH1RGEVOd!sLfQ zpy^Ad$h@op5)9*`r<@ExITrF+S^&cLXoZq_Xru<6=^YvyH2I}PRk`t!UhvyXl56m4 ziBhO?l!E<^*|c3R*j+bpG-@G$l|WVDT(`96S`Hg6hu0>ImfrP<(enM3(G9UNC$<}6 z`|4~K%Om*)@a=tH_}2XEo%buBi2eEY)eq>4J+z_C5ZgW{_ZB}}d@3Gz9>^<)VjSXt z5-MxcP{F&x9c3-}=g69?4A^p(;1XPj&mjAU;e=``XmZ%Q=~Nobe}sK2Am(hX4kiJZ z20QL9;xqXh%>EWK46ETOCw~W-V+f-@FFw;|)vUcR0i%0SO%?A`N!Lvri#B=5KINgF z>fiH**qak0h8TG$WW~s1@$3#tIM3(fFy&vOglSyb17`pXrVIu*2Nm5R^_A=kQ$f!& zhq4QqLo$%*pjjM?s3u;R(cw#xv#`AvXVIG=vn?a*N&+trK>URE$3o9`dk4)yDrZ|z zv2GA1JP=ogr_%Hm&?YNjNP191S#gLk4|fK6xplKe=#7I5;DD#@pYT*NN5KQ9(Q>$1 zhiExOW#C?wMW3ANM;BOvNH~r8PkP8ZOzlMU~UKIzX$pkc*;ew`<>DQ4cd{rtLMLTGFRQl&QNeyIw zyvbxQU(H>fGcMDB^kq9By$||G2WHfZM_N%?pTL%G%rYtS zxv9jnhY?=OfUDU~X0dO)z4sOj*O6`r@q)_!B%N#MG8($R4s>noJ^mo^=>6=~Xm;+p z(MVR#qVF5J8(JOw^1>Gvz=vx&$37T(*!i&jLHOzT_3YA*!J&gT=y``di{2ePtc+}k z4LPyZ5L;LGX0hztIMA~)XuA}sXQuR^ z!3@n5E7DFmmc&O4Srg>RSf~mwQIl{l440_o(dc|Cp3$%_Mx)okq_TUeqR|3>V{3Ih`gdTp=rZG+hK@wvFGnHSDS5%eTU{eAiYt$G^2D2Kc_MWe$#qai0tbnpbg{XG3|9m)Y~(a)i|KnUEoSbR81?m-8h j5FO)m&%GXx=Qm94@0j54nD+nXMp#eR76i{d){6fNce`4y literal 0 HcmV?d00001 diff --git a/__pycache__/server.cpython-313.pyc b/__pycache__/server.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c2a6cbab906e7fe4926188dcff7ab0d45c64b62 GIT binary patch literal 2375 zcmZuy-EY%Y6u)*H$4TroN!w65*19lgvqCpfT0x;5Aq}M}O;DUnh_s7BY|@D0RM&3V zDorX+170WyL?=xen)Xlx5|uV-llC<2A4o>FWG31s?TNQSUl30_*GZaC^+Y-M`h5A^ zbI1{$d3K>V9VPZVD2|4xsz^lBT?jdmh%%@Lcq3wzylV%=>4sl7nm&opsHGcSq5M?X zZ!v7?`K+Sqn!#v_DjO_TuH}{#qPV7LS}LO$Le(Ov<3%BnNzxs*OQ69j$%ggG^(&46}E!2=6l81~CJmy**dO~+2~x5Hl>2DXC!@$S9J zmYtqIoclLvi_lwk`yM*)I@c!tbbs*LhFUw{Y;u&)TXFAs=(_7#TPnE^z14{SJx?FK zz9&Fi->EqV8nfnypk`3>TICSjZk%!wJ5|SZWra2~K z#x zCcqFB1`3#AP0!~vXg*AiXSiyOmy%@+y@*M@v|8I?GSG%Fmr~_D<-zWLr(|K>8$e*e3?W>t0f8>>boGouQU5wpJTq2Lc`2&Ob z!HK;0T#=uw@U3^kx58_qMc!ZL_m=qX0^eQad;a1*Z#6e$cCRDQfu8gO`?X{EjN6h1 z**1|nn2qa(mS_fx=(b5?v%h)tn<1xW(gX~o$UCUsbSi27?PhEkHFX(f8`R#u}%ZM!1my3_rZ9IXi&%Dn$XTcM{y3SeIS&9-}4p1RXt4 zmo(OTW&jLHg#5PfOM(k83Ct{uiOKL}a1w@20)WlRIV{4=Edl_ma!yoK(V#{a_k(4k zX+Fb?%2LXl>Cdyj{h*RDl@-xX8?2tf8Nhc27R~Q9*c#s30%wzx;ZV$QN|E4HQi?@G zfoa2`DLOI8E$D_rikwdpB^dEiBpD6GrBGmeJZdnPv+zw}&PAk{!CZ($V+K2))pSM0 z-b*Sn9U!P3S6wjT`F*wirmaAJD|-Lq>ua@?t}#&BCLlna=TUhB%cAZP?#L~ zF~a>sV4@^<>RL`-oL9#15s;Do(GCOKv``fF0tqjWdy_?+XN4)-oWLCXitRPSQoXO; O0_EN6a#FLD8SQ^iYQszb literal 0 HcmV?d00001