Z8b6FbJ)@`Q@o}-On-on)#qK8017E
zcI`QNq75=UssG$5o4%4*!c9d`$@aX)XHjNzqc0c=bOat7U9Q=3C&ENmF$Gi5b`-%w
zw93jaXkra$xKU&(He||x{0j^M&cuE_b(EX{+k3j`D~zc1*|r2*S^&T3@Rxn$-?Ntm
zjSc{us$(9KRNgSQl|}o)$d=-i+X$=ofNcVdt%7nzX6z{KK1W4We^v|i?&%D2x?NK8
zd9{4wo7bV&HC_vE$bx+^@{FVKoZ@Q;+`XnB7W#|~g7;uD4{9W?C?!LTyc}qZk`Bi`Bfl+k{qjV+P;V|
zyLPF@1)Z|6q5qv!yu-l^&c0iNLsuJH)pTo~mX^D`*TiJ37Y~W58<^7>%os)xC7E8N
zc=ZlUme*{3GmSahhe5A*ZDxRpGrb?rn{FSE92zmfwr#jQkB|E`+<+|g3t<+ctv8;VJo<5o%Z+BW2Yr+>W8)G`jq%?yVuFmH0r_
z4Fr!L#N>{Ne!k(?=tZ{
zO|40LB1cYmDtAK6>z@T^P0$uM-ZctzRK&_O+;Kb6#fFBPKTQsXiS}{h{97UFcv5<=
ze8-`LEYCmx;Y=-;fe>NzX8qc{?@Sq>vK7)EurTtrJ2w7kutAEDFTQ!f`9M5Xu;M6m
zfDa{3fUps_ZRqW7IT`HEoK#pehjsAV7J+rplf&LuZd&iL+5M($H!+}psx>2S
zsxG?+FN}yU7*aT17H}p3c7|S3a<*Z0Sqwtm%AM+piDGGlhuX<8=2*q|_pHc5l9Z`-@Osw(`u4SdIzoV#1m<+XA6Z?Z2AfU|?E
JeYqVn{y)4=Y%u@;
literal 0
HcmV?d00001
diff --git a/src/azul/screenshots/Crash_at_Wed_Nov_13_16:02:46_2024.png b/src/azul/screenshots/Crash_at_Wed_Nov_13_16:02:46_2024.png
new file mode 100644
index 0000000000000000000000000000000000000000..61fe23da5f7bd80c3974a176ae8a96a0729e1ea0
GIT binary patch
literal 3888
zcmeHKX;4#H8htFT5OAcSScLT0A~LNIiH&R_wyh!~1=vH6IG}(m0V0SZO9%u?G{{83
zr5Q*A(a~|G**YKz!~hcnGzlTHBq5P4fe0jFCxk$l7Ztnx)j#IP%nxtXse9|yd$+!O
z?)kp+yV#EypJCG9sqRz4kB=)F8CyN(0>&GjFjGoz70q%k-uRkZRhD8
zIG)P1uaVlgZxoL0+T_2LjRROmNcWp{^rFi6|N@Rj33L`86XK7Sl@4Z4*JssD}^O!38Om-zP@z+`(aeS4=fK
zUB)^{B6dzl7;0YGc|X0s4_
zx&Wg)p1Hb`vebO>n!3htQa-_}9H3{Yam8$EvobEB*UzGUHpG@UCMc|YLU(qo4^O5C
zAeRNRCza{Ft&f&l^54wr`*FD&oD^M7qR`sE$wi}ckR#C9dD3s`harN+bhe{ZP4P$
ze9yz+i|3qi>DobS;b-079j|vF&jZAZxWHwNdNNa^l3q=rB<`isHZ~@nE$fMuVvaD4
zBj!SES%*ns({f>5eyM)8W#gaY@oV8!Qy^Gm#K`#K4;SDp5;e`suiN8(qh@JhD&~$(
z@~scNfR^uI4XBe-{NCqG<5h+@
zx)pKEm5&F|9{9y5TtVo0tM@AUhd~Y&M&xmVtRq``Fv3$!ghKBk#D}A3kMp~L|C4vy
z)VMO)FvFL^o|6zUyY}(^jV?IZ4)3z`@uW>~m5Lu?$61$y{cpLxbvn@&WZUJ0jWX&V
zqd1(d_UVujPZE!qNUd)XwIcXLTWcEoJpY<+pSk$2xhR2-+5kKDNNt);JSwCxs)7bn&XO5inqi@;=>R8`ZVbL)aZ})Ig-)SA@YF2}0n;SDXZUrZP{n
zLWktVHwNwo&0);S(TpHf(pS0eITx3*5olPGQtKiz%k?G#8Ll?zb@laf)d)TD_VcsP
zCjTPIsB>E;J1d`2RlN}9b=8!rJIk1iHV=;FRb?=#Vp6eeYn@wUHrd&o`??}*>eaBb
zHhJ;6!Q7$1N5Y@-?lgn~vhW4agQ7TFl1`y!`dpFyJ`d}XQF?j5pv`@1(0p!CX;MT&
z1h0zWsWD6q8CIOs8jJRQ7ODh1ceT}p7#2C3SX5zM#)wdM7I8_l=oS8xGNqg?;
z@hl$_MTS*P>i%e_ars&&!2u*>`V^zz*@L3&&25OZhIVXHUyXYs+{v-)9}l-%=bnc2
z%2b3UL$V=Ss=yFYV5!|jM)6_T-}u@md4{iJq;}LteIdROU~2dhPP^Ypo~mJ(Vel``
zOvktGG&R90FBiKp&QvG#!cx5or6cl2IBkJAw{okCkLH^{kJ-ZM;RWHIQ`bB^HvUn6@4^HW1B%CYQZqvs%BiJ
zYF*9VQ^@dgA8;8QEkZCJE1s|ke}k~na_`yL+CHj4EEXq-V;*vY!r>`77*OV|iD!9L
zCMGV&?2)SP2I3Wa*0p{#mk$C<344ATy7-lUI1gs-m&&WUuZ-crj)GSI{bl_nRM|*Cq?NiOdc6+=lsCyc8KUMl_I+bmL0EH5
zZNE{CHfPDkv?E4w9N05s2*;+S_4T1Go3z3N+ltm%s$Q8x8oxlVY_R^F!Es1oZb6BH
zj1x)e0$Izk19?d??S;^KqJ9Hl%VG{YUz75O7le4M!=b%2)lkpqd|JjUqsT{&@lYGS
zMSgqG=4;K`y}?LReMyJ`u=O!7)P}y+%X~Utlg=02W1y_5h`gtlxG&+sEh}Fv;cV=E
zyh&jJ*<)hF#fdlJ`;zd}E?|;1$x~GB{obJgQJe%FW$#lgmX?44*WUY)*B9377XA^u
nr|>lg|JV!uwlDl2tTo2S=;u+R-KN@qW5D~c@1cr=p_l#xcDVQH
literal 0
HcmV?d00001
diff --git a/src/azul/screenshots/Crash_at_Wed_Nov_13_16:20:10_2024.png b/src/azul/screenshots/Crash_at_Wed_Nov_13_16:20:10_2024.png
new file mode 100644
index 0000000000000000000000000000000000000000..0d712ac54d6b6fef95167d14f1aa4de2d93bf541
GIT binary patch
literal 3886
zcmeHKYgCfi8vfKN%j2cf98)Vb7c<(K%rqKOBe6zvoD#cCDxs-aFy$pfO9j!=Nhfbq
zR_16pMMkN4iBbsxl}SSpa#GO*#lkzNA%cjPftoXGwbog4{+;>bx7PFLeZRH8y`TMj
z&$IWHgK#e+gY^ag02q0H>v0$WbTc#vSf#6}oDm&d0s!L~Z;w6S#}`V4z31Ns=(-;Q
zmmV3(A-_AfR`kVJ83v7(9(L`fpG^-OHSg`NJ1GQp<5a98dV3q!a|M}JrRh>V;Hr$-
zz6Aj4?0CRc(BFXR<`Gj-U!*^Y4;UjNy()B+kuxD?C)b2N@V#&GLJSG}0h}4)b?RRU
zD8M?w{wLv0n*&kc8a?X^NSuvRsgm{x5rrCh<(Re>_49EB$K+aC&usz9Z)i1QGtB{<
zi*9m?wdFdrILizHmp{STUIxD~_j(&nJQ|Lf%j8loksyi`@>xhhD`a4##k~ls=bocQ
z>-AUXqoZQz$CKxq$z(Et=X&Z)>{t5yv4<-nMT}O6LhdPrC`Lz5E7cb!Hcfx(H_Xd>IqMmIJDF9Vmo4?J4dmy#-%UsR|1LBu|z_}^|Q4BA06}D5bId0>>*ldXJlBGvTX2?mFu><}Rc-e_HBsB2y
zng-_%`A+cLaT|lE50PIFG$rBJc3|ml*jpXV{~m8c#Y_>L=i$N8>-u+tOFQCttcuOl
zSCd4vdT!|kG+B-y#tEu2JRPYPE~m;}<%~UxK$|
z|Cqa-UqeuL+sFh@Ig!N7F7RvE>hzzMc4_^)WBy?aH6@>JG=j!gcP`4*lf7+A)PaT(
z3m`>o0bmY9y?&OAzAyAK&glb%zHB4GnK7(IklE8qYKPwL)m!EGR7&kbJBsW0J)*k2
z4)i)MxMMoD2=D3IyOKClwtAf0%)~jCFw`G~1{|7&+4?6cF
z`2&nZJTY+Z|Ve^$U|mW
z0jk~ta_YpaL)XyxS$I3dDuJQat29<-@nC)TohOC34Jmr+ETYb)<`Ih=TwKGG87zIt
zx`;@G2Xbk9rfGw9ND-(1BonzaEY3W9MwFb&84_>!j`($Y9r6u|;u6sEI(UNOm2{_^
z5RU7?aPi$w%M;tZ7kM74?&o}^UCkMRt=6W(FdNDTvo?ib_GywjOwKt`#vd2j97|qW
zP{NkxV+M66&s3@>Myg41!yCs1;iTgiDs^wL!Gg{Jc^-a7uB5yNvVp&J6us5Wc@IkU${N
zdS-cybhlY@VqzlH`7CouB%D-xf!(vAtf1M5@98v54{aM6M!VN@$PD(+ZaZEy{{pM|
zQH~|p>aw@<4nqKwMMT^m9v&7>GT?BR+2QCCr@~f^!)xlHm74kj`(p8~_~@Azu7U{N~MzT+Bf~o8kRIauZoc$tN%=FQERE0?sEy&_TW1jy0&Yqrj^L(iKyvy@WCctz0_G#Sz1(2BK=pEhkhIj!1$RMzX^dbnPZPFw>(u3$y!Vc
z&baK~!LH8hujhkIF{MqBAJa8WX*Ru{QTT$Yx(nv`5(ly$wP0bFi6{3@4og!$XM>^8
z195J0`W=5dGuw=S)#}(hJdP@B+~*gaelI@P3*
z;!X1}IkLQDhWe{kI^Oy;n^1DKI#B<3l|8rQM3RhSZqRnP+uvd@F`wTjVv()zXBCIP
zL8HVap;^DQ+UF3y8#*QDta2L-SckKxfU&5}tRLPc?%n>=a_B7w)~8m#%*Qex|6Kzr
Z*u|0+Jcw}~SaS~syglI_<$FU?eg|T+PbmNZ
literal 0
HcmV?d00001
diff --git a/src/azul/server.py b/src/azul/server.py
new file mode 100755
index 0000000..cfe9eab
--- /dev/null
+++ b/src/azul/server.py
@@ -0,0 +1,870 @@
+#!/usr/bin/env python3
+# Checkers Game Server
+
+"""Checkers Game Server."""
+
+# Programmed by CoolCat467
+
+from __future__ import annotations
+
+# Copyright (C) 2023-2024 CoolCat467
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+__title__ = "Server"
+__author__ = "CoolCat467"
+__license__ = "GNU General Public License Version 3"
+__version__ = "0.0.0"
+
+import time
+import traceback
+from collections import deque
+from functools import partial
+from typing import TYPE_CHECKING, NoReturn, cast
+
+import trio
+
+from azul import network
+from azul.base_io import StructFormat
+from azul.buffer import Buffer
+from azul.component import ComponentManager, Event, ExternalRaiseManager
+from azul.encrypted_event import EncryptedNetworkEventComponent
+from azul.encryption import (
+ RSAPrivateKey,
+ decrypt_token_and_secret,
+ generate_rsa_key,
+ generate_verify_token,
+ serialize_public_key,
+)
+from azul.network_shared import (
+ ADVERTISEMENT_IP,
+ ADVERTISEMENT_PORT,
+ DEFAULT_PORT,
+ ClientBoundEvents,
+ ServerBoundEvents,
+ find_ip,
+)
+from azul.state import State, generate_pieces
+
+if TYPE_CHECKING:
+ from collections.abc import Awaitable, Callable, Iterable
+
+
+class ServerClient(EncryptedNetworkEventComponent):
+ """Server Client Network Event Component.
+
+ When clients connect to server, this class handles the incoming
+ connections to the server in the way of reading and raising events
+ that are transferred over the network.
+ """
+
+ __slots__ = ("client_id", "rsa_key", "verify_token")
+
+ def __init__(self, client_id: int) -> None:
+ """Initialize Server Client."""
+ self.client_id = client_id
+ super().__init__(f"client_{client_id}")
+
+ self.timeout = 3
+
+ cbe = ClientBoundEvents
+ self.register_network_write_events(
+ {
+ "server[write]->encryption_request": cbe.encryption_request,
+ "server[write]->callback_ping": cbe.callback_ping,
+ "server[write]->initial_config": cbe.initial_config,
+ "server[write]->game_over": cbe.game_over,
+ "server[write]->playing_as": cbe.playing_as,
+ },
+ )
+ sbe = ServerBoundEvents
+ self.register_read_network_events(
+ {
+ sbe.encryption_response: f"client[{self.client_id}]->encryption_response",
+ },
+ )
+
+ self.rsa_key: RSAPrivateKey | None = None
+ self.verify_token: bytes | None = None
+
+ def bind_handlers(self) -> None:
+ """Bind event handlers."""
+ super().bind_handlers()
+ self.register_handlers(
+ {
+ f"callback_ping->network[{self.client_id}]": self.handle_callback_ping,
+ f"client[{self.client_id}]->encryption_response": self.handle_encryption_response,
+ "initial_config->network": self.handle_initial_config,
+ "game_over->network": self.handle_game_over,
+ f"playing_as->network[{self.client_id}]": self.handle_playing_as,
+ },
+ )
+
+ async def handle_game_over(self, event: Event[int]) -> None:
+ """Read game over event and reraise as server[write]->game_over."""
+ winner = event.data
+
+ buffer = Buffer()
+
+ buffer.write_value(StructFormat.UBYTE, winner)
+
+ await self.write_event(Event("server[write]->game_over", buffer))
+
+ async def handle_initial_config(
+ self,
+ event: Event[tuple[Pos, int]],
+ ) -> None:
+ """Read initial config event and reraise as server[write]->initial_config."""
+ board_size, player_turn = event.data
+
+ buffer = Buffer()
+
+## write_position(buffer, board_size)
+ buffer.write_value(StructFormat.UBYTE, 0)
+ buffer.write_value(StructFormat.UBYTE, player_turn)
+
+ await self.write_event(Event("server[write]->initial_config", buffer))
+
+ async def handle_playing_as(
+ self,
+ event: Event[int],
+ ) -> None:
+ """Read playing as event and reraise as server[write]->playing_as."""
+ playing_as = event.data
+
+ buffer = Buffer()
+ buffer.write_value(StructFormat.UBYTE, playing_as)
+ await self.write_event(Event("server[write]->playing_as", buffer))
+
+ async def write_callback_ping(self) -> None:
+ """Write callback_ping packet to client.
+
+ Could raise the following exceptions:
+ trio.BrokenResourceError: if something has gone wrong, and the stream
+ is broken.
+ trio.ClosedResourceError: if stream was previously closed
+
+ Listed as possible but probably not because of write lock:
+ trio.BusyResourceError: if another task is using :meth:`write`
+ """
+ buffer = Buffer()
+
+ # Try to be as accurate with time as possible
+ await self.wait_write_might_not_block()
+ ns = int(time.time() * 1e9)
+ # Use as many bits as time needs, write_buffer handles size for us.
+ buffer.write(ns.to_bytes(-(-ns.bit_length() // 8), "big"))
+
+ await self.write_event(Event("server[write]->callback_ping", buffer))
+
+ async def handle_callback_ping(
+ self,
+ _: Event[None],
+ ) -> None:
+ """Reraise as server[write]->callback_ping."""
+ await self.write_callback_ping()
+
+ async def start_encryption_request(self) -> None:
+ """Start encryption request and raise as server[write]->encryption_request."""
+ if self.encryption_enabled:
+ raise RuntimeError("Encryption is already set up!")
+ self.rsa_key = generate_rsa_key()
+ self.verify_token = generate_verify_token()
+
+ public_key = self.rsa_key.public_key()
+
+ serialized_public_key = serialize_public_key(public_key)
+
+ buffer = Buffer()
+ buffer.write_bytearray(serialized_public_key)
+ buffer.write_bytearray(self.verify_token)
+
+ await self.write_event(
+ Event("server[write]->encryption_request", buffer),
+ )
+
+ event = await self.read_event()
+ if event.name != f"client[{self.client_id}]->encryption_response":
+ raise RuntimeError(
+ f"Expected encryption response, got but {event.name!r}",
+ )
+ await self.handle_encryption_response(event)
+
+ async def handle_encryption_response(
+ self,
+ event: Event[bytearray],
+ ) -> None:
+ """Read encryption response."""
+ if self.rsa_key is None or self.verify_token is None:
+ raise RuntimeError(
+ "Was not expecting encryption response, request start not sent!",
+ )
+ if self.encryption_enabled:
+ raise RuntimeError("Encryption is already set up!")
+ buffer = Buffer(event.data)
+
+ encrypted_shared_secret = buffer.read_bytearray()
+ encrypted_verify_token = buffer.read_bytearray()
+
+ verify_token, shared_secret = decrypt_token_and_secret(
+ self.rsa_key,
+ encrypted_verify_token,
+ encrypted_shared_secret,
+ )
+
+ if verify_token != self.verify_token:
+ raise RuntimeError(
+ "Received verify token does not match sent verify token!",
+ )
+
+ # Start encrypting all future data
+ self.enable_encryption(shared_secret, verify_token)
+
+
+class GameServer(network.Server):
+ """Checkers server.
+
+ Handles accepting incoming connections from clients and handles
+ main game logic via State subclass above.
+ """
+
+ __slots__ = (
+ "actions_queue",
+ "advertisement_scope",
+ "client_count",
+ "client_players",
+ "internal_singleplayer_mode",
+ "player_selections",
+ "players_can_interact",
+ "running",
+ "state",
+ )
+
+ board_size = (8, 8)
+ max_clients = 4
+
+ def __init__(self, internal_singleplayer_mode: bool = False) -> None:
+ """Initialize server."""
+ super().__init__("GameServer")
+
+ self.client_count: int
+ self.state: CheckersState = State(self.board_size, {})
+
+ self.client_players: dict[int, int] = {}
+ self.player_selections: dict[int, Pos] = {}
+ self.players_can_interact: bool = False
+
+ self.internal_singleplayer_mode = internal_singleplayer_mode
+ self.advertisement_scope: trio.CancelScope | None = None
+ self.running = False
+
+ def bind_handlers(self) -> None:
+ """Register start_server and stop_server."""
+ self.register_handlers(
+ {
+ "server_start": self.start_server,
+ "network_stop": self.stop_server,
+ "server_send_game_start": self.handle_server_start_new_game,
+ "network->select_piece": self.handle_network_select_piece,
+ "network->select_tile": self.handle_network_select_tile,
+ },
+ )
+
+ async def stop_server(self, event: Event[None] | None = None) -> None:
+ """Stop serving and disconnect all NetworkEventComponents."""
+ self.stop_serving()
+ self.stop_advertising()
+
+ close_methods: deque[Callable[[], Awaitable[object]]] = deque()
+ for component in self.get_all_components():
+ if isinstance(component, network.NetworkEventComponent):
+ close_methods.append(component.close)
+ print(f"stop_server {component.name = }")
+ self.remove_component(component.name)
+ async with trio.open_nursery() as nursery:
+ while close_methods:
+ nursery.start_soon(close_methods.popleft())
+ self.running = False
+
+ async def post_advertisement(
+ self,
+ udp_socket: trio.socket.SocketType,
+ send_to_ip: str,
+ hosting_port: int,
+ ) -> None:
+ """Post server advertisement packet."""
+ motd = "Azul Game"
+ advertisement = (
+ f"[AD]{hosting_port}[/AD][AZUL]{motd}[/AZUL]"
+ ).encode()
+ # print("post_advertisement")
+ await udp_socket.sendto(
+ advertisement,
+ (send_to_ip, ADVERTISEMENT_PORT),
+ )
+
+ def stop_advertising(self) -> None:
+ """Cancel self.advertisement_scope."""
+ if self.advertisement_scope is None:
+ return
+ self.advertisement_scope.cancel()
+
+ async def post_advertisements(self, hosting_port: int) -> None:
+ """Post lan UDP packets so server can be found."""
+ self.stop_advertising()
+ self.advertisement_scope = trio.CancelScope()
+
+ # Look up multicast group address in name server and find out IP version
+ addrinfo = (await trio.socket.getaddrinfo(ADVERTISEMENT_IP, None))[0]
+ send_to_ip = addrinfo[4][0]
+
+ with trio.socket.socket(
+ family=trio.socket.AF_INET, # IPv4
+ type=trio.socket.SOCK_DGRAM, # UDP
+ proto=trio.socket.IPPROTO_UDP, # UDP
+ ) as udp_socket:
+ # Set Time-to-live (optional)
+ # ttl_bin = struct.pack('@i', MYTTL)
+ # if addrinfo[0] == trio.socket.AF_INET: # IPv4
+ # udp_socket.setsockopt(
+ # trio.socket.IPPROTO_IP, trio.socket.IP_MULTICAST_TTL, ttl_bin)
+ # else:
+ # udp_socket.setsockopt(
+ # trio.socket.IPPROTO_IPV6, trio.socket.IPV6_MULTICAST_HOPS, ttl_bin)
+ with self.advertisement_scope:
+ print("Starting advertisement posting.")
+ while True: # not self.can_start():
+ try:
+ await self.post_advertisement(
+ udp_socket,
+ send_to_ip,
+ hosting_port,
+ )
+ except OSError as exc:
+ traceback.print_exception(exc)
+ print(
+ f"{self.__class__.__name__}: Failed to post server advertisement",
+ )
+ break
+ await trio.sleep(1.5)
+ print("Stopped advertisement posting.")
+
+ @staticmethod
+ def setup_teams_internal(client_ids: list[int]) -> dict[int, int]:
+ """Return teams for internal server mode given sorted client ids."""
+ players: dict[int, int] = {}
+ for idx, client_id in enumerate(client_ids):
+ if idx == 0:
+ players[client_id] = 2
+ else:
+ players[client_id] = 0xFF # Spectator
+ return players
+
+ @staticmethod
+ def setup_teams(client_ids: list[int]) -> dict[int, int]:
+ """Return teams given sorted client ids."""
+ players: dict[int, int] = {}
+ for idx, client_id in enumerate(client_ids):
+ if idx < 2:
+ players[client_id] = idx % 2
+ else:
+ players[client_id] = 0xFF # Spectator
+ return players
+
+ def new_game_init(self) -> None:
+ """Start new game."""
+ self.client_players.clear()
+ self.player_selections.clear()
+
+ pieces = generate_pieces(*self.board_size)
+ self.state = State(self.board_size, pieces)
+
+ # Why keep track of another object just to know client ID numbers
+ # if we already have that with the components? No need!
+ client_ids: set[int] = set()
+ for component in self.get_all_components():
+ if isinstance(component, ServerClient):
+ client_ids.add(component.client_id)
+
+ sorted_client_ids = sorted(client_ids)
+ if self.internal_singleplayer_mode:
+ self.client_players = self.setup_teams_internal(sorted_client_ids)
+ else:
+ self.client_players = self.setup_teams(sorted_client_ids)
+
+ self.players_can_interact = True
+
+ # "Implicit return in function which does not return"
+ async def start_server( # type: ignore[misc]
+ self,
+ event: Event[tuple[str | None, int]],
+ ) -> NoReturn:
+ """Serve clients."""
+ print(f"{self.__class__.__name__}: Closing old server clients")
+ await self.stop_server()
+ print(f"{self.__class__.__name__}: Starting Server")
+ self.client_count = 0
+
+ host, port = event.data
+
+ self.running = True
+ async with trio.open_nursery() as nursery:
+ # Do not post advertisements when using internal singleplayer mode
+ if not self.internal_singleplayer_mode:
+ nursery.start_soon(self.post_advertisements, port)
+ # Serve runs forever until canceled
+ nursery.start_soon(partial(self.serve, port, host, backlog=0))
+
+ async def transmit_playing_as(self) -> None:
+ """Transmit playing as."""
+ async with trio.open_nursery() as nursery:
+ for client_id, team in self.client_players.items():
+ nursery.start_soon(
+ self.raise_event,
+ Event(f"playing_as->network[{client_id}]", team),
+ )
+
+ async def handle_server_start_new_game(self, event: Event[None]) -> None:
+ """Handle game start."""
+## # Delete all pieces from last state (shouldn't be needed but still.)
+## async with trio.open_nursery() as nursery:
+## for piece_pos, _piece_type in self.state.get_pieces():
+## nursery.start_soon(
+## self.raise_event,
+## Event("delete_piece->network", piece_pos),
+## )
+
+ # Choose which team plays first
+ # Using non-cryptographically secure random because it doesn't matter
+ self.new_game_init()
+
+## # Send create_piece events for all pieces
+## async with trio.open_nursery() as nursery:
+## for piece_pos, piece_type in self.state.get_pieces():
+## nursery.start_soon(
+## self.raise_event,
+## Event("create_piece->network", (piece_pos, piece_type)),
+## )
+
+ await self.transmit_playing_as()
+
+ # Raise initial config event with board size and initial turn.
+ await self.raise_event(
+ Event(
+ "initial_config->network",
+ (self.board_size, self.state.turn),
+ ),
+ )
+
+ async def client_network_loop(self, client: ServerClient) -> None:
+ """Network loop for given ServerClient.
+
+ Could raise the following exceptions:
+ trio.BrokenResourceError: if something has gone wrong, and the stream
+ is broken.
+ trio.ClosedResourceError: if stream was previously closed
+
+ Probably couldn't raise because of write lock but still:
+ trio.BusyResourceError: More than one task is trying to write
+ to socket at once.
+ """
+ while not self.can_start() and not client.not_connected:
+ try:
+ await client.write_callback_ping()
+ except (
+ trio.BrokenResourceError,
+ trio.ClosedResourceError,
+ network.NetworkStreamNotConnectedError,
+ ):
+ print(f"{client.name} Disconnected in lobby.")
+ return
+ while not client.not_connected:
+ event: Event[bytearray] | None = None
+ try:
+ await client.write_callback_ping()
+ with trio.move_on_after(2):
+ event = await client.read_event()
+ except network.NetworkTimeoutError:
+ print(f"{client.name} Timeout")
+ break
+ except network.NetworkEOFError:
+ print(f"{client.name} EOF")
+ break
+ except (
+ trio.BrokenResourceError,
+ trio.ClosedResourceError,
+ RuntimeError,
+ ):
+ break
+ except Exception as exc:
+ traceback.print_exception(exc)
+ break
+ if event is not None:
+ # print(f"{client.name} client_network_loop tick")
+ # print(f"{client.name} {event = }")
+ await client.raise_event(event)
+
+ def can_start(self) -> bool:
+ """Return if game can start."""
+ if self.internal_singleplayer_mode:
+ return self.client_count >= 1
+ return self.client_count >= 2
+
+ def game_active(self) -> bool:
+ """Return if game is active."""
+ return self.state.check_for_win() is None
+
+ async def send_spectator_join_packets(
+ self,
+ client: ServerClient,
+ ) -> None:
+ """Send spectator start data."""
+ print("send_spectator_join_packets")
+
+ private_events_pocket = ComponentManager(
+ f"private_events_pocket for {client.client_id}",
+ )
+ with self.temporary_component(private_events_pocket):
+ with private_events_pocket.temporary_component(client):
+## # Send create_piece events for all pieces
+## async with trio.open_nursery() as nursery:
+## for piece_pos, piece_type in self.state.get_pieces():
+## nursery.start_soon(
+## client.raise_event,
+## Event(
+## "create_piece->network",
+## (piece_pos, piece_type),
+## ),
+## )
+
+ await client.raise_event(
+ Event(f"playing_as->network[{client.client_id}]", 255),
+ )
+
+ # Raise initial config event with board size and initial turn.
+ await client.raise_event(
+ Event(
+ "initial_config->network",
+ (self.state.size, self.state.turn),
+ ),
+ )
+
+ async def handler(self, stream: trio.SocketStream) -> None:
+ """Accept clients. Called by network.Server.serve."""
+ if self.client_count == 0 and self.game_active():
+ # Old game was running but everyone left, restart
+ self.state.pieces.clear()
+ # self.state = CheckersState(self.board_size, {})
+ new_client_id = self.client_count
+ print(
+ f"{self.__class__.__name__}: client connected [client_id {new_client_id}]",
+ )
+ self.client_count += 1
+
+ can_start = self.can_start()
+ game_active = self.game_active()
+ # if can_start:
+ # self.stop_serving()
+
+ if self.client_count > self.max_clients:
+ print(
+ f"{self.__class__.__name__}: client disconnected, too many clients",
+ )
+ await stream.aclose()
+ self.client_count -= 1
+ return
+
+ async with ServerClient.from_stream(
+ new_client_id,
+ stream=stream,
+ ) as client:
+ # Encrypt traffic
+ await client.start_encryption_request()
+ assert client.encryption_enabled
+
+ if can_start and game_active:
+ await self.send_spectator_join_packets(client)
+ with self.temporary_component(client):
+ if can_start and not game_active:
+ await self.raise_event(
+ Event("server_send_game_start", None),
+ )
+ try:
+ await self.client_network_loop(client)
+ finally:
+ print(
+ f"{self.__class__.__name__}: client disconnected [client_id {new_client_id}]",
+ )
+ self.client_count -= 1
+ # ServerClient's `with` block handles closing stream.
+
+ async def handle_network_select_piece(
+ self,
+ event: Event[tuple[int, Pos]],
+ ) -> None:
+ """Handle piece event from client."""
+ client_id, tile_pos = event.data
+
+ player = self.client_players.get(client_id, 0xFF)
+ if player == 2:
+ player = int(self.state.turn)
+
+ if player != self.state.turn:
+ print(
+ f"{player = } cannot select piece {tile_pos = } because it is not that player's turn",
+ )
+ return
+
+ if not self.players_can_interact:
+ print(
+ f"{player = } cannot select piece {tile_pos = } because players_can_interact is False",
+ )
+ return
+ if not self.state.can_player_select_piece(player, tile_pos):
+ print(f"{player = } cannot select piece {tile_pos = }")
+ await self.player_select_piece(player, None)
+ return
+ if tile_pos == self.player_selections.get(player):
+ # print(f"{player = } toggle select -> No select")
+ await self.player_select_piece(player, None)
+ return
+
+ await self.player_select_piece(player, tile_pos)
+
+ async def player_select_piece(
+ self,
+ player: int,
+ piece_pos: Pos | None,
+ ) -> None:
+ """Update glowing tiles from new selected piece."""
+ ignore: set[Pos] = set()
+
+ if piece_pos is not None:
+ # Calculate actions if required
+ new_action_set = self.state.calculate_actions(piece_pos)
+ ignore = new_action_set.ends
+
+ ignored: set[Pos] = set()
+
+ # Remove outlined tiles from previous selection if existed
+ if prev_selection := self.player_selections.get(player):
+ action_set = self.state.calculate_actions(prev_selection)
+ ignored = action_set.ends & ignore
+ remove = action_set.ends - ignore
+ async with trio.open_nursery() as nursery:
+ for tile_position in remove:
+ nursery.start_soon(
+ self.raise_event,
+ Event("delete_tile->network", tile_position),
+ )
+ if piece_pos != prev_selection:
+ nursery.start_soon(
+ self.raise_event,
+ Event(
+ "select_piece->network",
+ (prev_selection, False),
+ ),
+ )
+
+ if piece_pos is None:
+ if prev_selection:
+ del self.player_selections[player]
+ return
+
+ self.player_selections[player] = piece_pos
+
+ # For each end point
+ async with trio.open_nursery() as nursery:
+ for tile_position in new_action_set.ends - ignored:
+ nursery.start_soon(
+ self.raise_event,
+ Event("create_tile->network", tile_position),
+ )
+ # Sent select piece as well
+ nursery.start_soon(
+ self.raise_event,
+ Event(
+ "select_piece->network",
+ (self.player_selections[player], True),
+ ),
+ )
+
+ async def handle_move_animation(self, from_pos: Pos, to_pos: Pos) -> None:
+ """Handle move animation."""
+ await self.raise_event(
+ Event("move_piece_animation->network", (from_pos, to_pos)),
+ )
+
+ async def handle_jump_animation(self, jumped_pos: Pos) -> None:
+ """Handle jump animation."""
+ await self.raise_event(
+ Event("delete_piece_animation->network", jumped_pos),
+ )
+
+ async def handle_king_animation(
+ self,
+ kinged_pos: Pos,
+ piece_type: int,
+ ) -> None:
+ """Handle jump animation."""
+ await self.raise_event(
+ Event("update_piece_animation->network", (kinged_pos, piece_type)),
+ )
+
+ async def handle_action_animations(
+ self,
+ actions: deque[tuple[str, Iterable[Pos | int]]],
+ ) -> None:
+ """Handle action animations."""
+ while actions:
+ name, params = actions.popleft()
+ if name == "move":
+ await self.handle_move_animation(
+ *cast("Iterable[Pos]", params),
+ )
+ elif name == "jump":
+ await self.handle_jump_animation(
+ *cast("Iterable[Pos]", params),
+ )
+ elif name == "king":
+ await self.handle_king_animation(
+ *cast("tuple[Pos, int]", params),
+ )
+ else:
+ raise NotImplementedError(f"Animation for action {name}")
+
+ async def handle_network_select_tile(
+ self,
+ event: Event[tuple[int, Pos]],
+ ) -> None:
+ """Handle select tile event from network."""
+ client_id, tile_pos = event.data
+
+ player = self.client_players.get(client_id, 0xFF)
+ if player == 2:
+ player = int(self.state.turn)
+
+ if not self.players_can_interact:
+ print(
+ f"{player = } cannot select tile {tile_pos = } because players_can_interact is False",
+ )
+ return
+
+ if player != self.state.turn:
+ print(
+ f"{player = } cannot select tile {tile_pos = } because it is not their turn.",
+ )
+ return
+
+ piece_pos = self.player_selections.get(player)
+ if piece_pos is None:
+ print(
+ f"{player = } cannot select tile {tile_pos = } because has no selection",
+ )
+ return
+
+ if tile_pos not in self.state.calculate_actions(piece_pos).ends:
+ print(
+ f"{player = } cannot select tile {piece_pos!r} because not valid move",
+ )
+ return
+
+ self.players_can_interact = False # No one moves during animation
+ # Send animation state start event
+ await self.raise_event(Event("animation_state->network", True))
+
+ # Remove tile sprites and glowing effect
+ await self.player_select_piece(player, None)
+
+ action = self.state.action_from_points(piece_pos, tile_pos)
+ # print(f"{action = }")
+
+ # Get new state after performing valid action
+ new_state = self.state.preform_action(action)
+ # Get action queue from old state
+ action_queue = self.state.get_action_queue()
+ self.state = new_state
+
+ # Send action animations
+ await self.handle_action_animations(action_queue)
+
+ # Send action complete event
+ await self.raise_event(
+ Event(
+ "action_complete->network",
+ (piece_pos, tile_pos, self.state.turn),
+ ),
+ )
+
+ win_value = self.state.check_for_win()
+ if win_value is not None:
+ # If we have a winner, send game over event.
+ await self.raise_event(Event("game_over->network", win_value))
+ return
+
+ # If not game over, allow interactions so next player can take turn
+ self.players_can_interact = True
+ await self.raise_event(Event("animation_state->network", False))
+
+ def __del__(self) -> None:
+ """Debug print."""
+ print(f"del {self.__class__.__name__}")
+ super().__del__()
+
+
+async def run_server(
+ server_class: type[GameServer],
+ host: str,
+ port: int,
+) -> None:
+ """Run machine client and raise tick events."""
+ async with trio.open_nursery() as main_nursery:
+ event_manager = ExternalRaiseManager(
+ "azul",
+ main_nursery,
+ )
+ server = server_class()
+ event_manager.add_component(server)
+
+ await event_manager.raise_event(Event("server_start", (host, port)))
+ while not server.running:
+ print("Server starting...")
+ await trio.sleep(1)
+
+ print("\nServer running.")
+
+ try:
+ while server.running: # noqa: ASYNC110 # sleep in while loop
+ # Process background tasks in the main nursery
+ await trio.sleep(0.01)
+ except KeyboardInterrupt:
+ print("\nClosing from keyboard interrupt.")
+ await server.stop_server()
+ server.unbind_components()
+
+
+async def cli_run_async() -> None:
+ """Run game server."""
+ host = await find_ip()
+ port = DEFAULT_PORT
+ await run_server(GameServer, host, port)
+
+
+def cli_run() -> None:
+ """Run game server."""
+ trio.run(cli_run_async)
+
+
+if __name__ == "__main__":
+ cli_run()
diff --git a/src/azul/sound.py b/src/azul/sound.py
new file mode 100644
index 0000000..1576592
--- /dev/null
+++ b/src/azul/sound.py
@@ -0,0 +1,67 @@
+"""Sound - Play sounds."""
+
+# Programmed by CoolCat467
+
+from __future__ import annotations
+
+# Sound - Play sounds
+# Copyright (C) 2024 CoolCat467
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+__title__ = "sound"
+__author__ = "CoolCat467"
+__version__ = "0.0.0"
+__license__ = "GNU General Public License Version 3"
+
+
+from typing import TYPE_CHECKING, NamedTuple
+
+from pygame import mixer
+
+if TYPE_CHECKING:
+ from os import PathLike
+
+
+class SoundData(NamedTuple):
+ """Sound data container."""
+
+ loops: int = 0
+ maxtime: int = 0
+ fade_ms: int = 0
+ volume: int = 100
+ # volume_left: int = 100
+ # volume_right: int = 100
+
+
+def play_sound( # pragma: nocover
+ filename: PathLike[str] | str,
+ sound_data: SoundData,
+) -> tuple[mixer.Sound, int | float]:
+ """Play sound with pygame."""
+ sound_object = mixer.Sound(filename)
+ sound_object.set_volume(sound_data.volume)
+ seconds: int | float = sound_object.get_length()
+ if sound_data.maxtime > 0:
+ seconds = sound_data.maxtime
+ _channel = sound_object.play(
+ loops=sound_data.loops,
+ maxtime=sound_data.maxtime,
+ fade_ms=sound_data.fade_ms,
+ )
+ # channel.set_volume(
+ # sound_data.volume_left,
+ # sound_data.volume_right,
+ # )
+ return sound_object, seconds
diff --git a/src/azul/state.py b/src/azul/state.py
new file mode 100644
index 0000000..114a6a9
--- /dev/null
+++ b/src/azul/state.py
@@ -0,0 +1,461 @@
+"""Checkers State."""
+
+# Programmed by CoolCat467
+
+from __future__ import annotations
+
+# Copyright (C) 2023-2024 CoolCat467
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+__title__ = "Checkers State"
+__author__ = "CoolCat467"
+__license__ = "GNU General Public License Version 3"
+__version__ = "0.0.0"
+
+import copy
+import math
+from dataclasses import dataclass
+from typing import (
+ TYPE_CHECKING,
+ NamedTuple,
+ TypeAlias,
+ TypeVar,
+ cast,
+)
+
+from mypy_extensions import u8
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, Generator, Iterable
+
+ from typing_extensions import Self
+
+MANDATORY_CAPTURE = True # If a jump is available, do you have to or not?
+PAWN_JUMP_FORWARD_ONLY = True # Pawns not allowed to go backwards in jumps?
+
+# Note: Tile Ids are chess board tile titles, A1 to H8
+# A8 ... H8
+# .........
+# A1 ... H1
+
+# Player:
+# 0 = False = Red = MIN = 0, 2
+# 1 = True = Black = MAX = 1, 3
+
+T = TypeVar("T")
+
+Pos: TypeAlias = tuple[u8, u8]
+
+
+class Action(NamedTuple):
+ """Represents an action."""
+
+ from_pos: Pos
+ to_pos: Pos
+
+
+class ActionSet(NamedTuple):
+ """Represents a set of actions."""
+
+ jumps: dict[Pos, list[Pos]]
+ moves: tuple[Pos, ...]
+ ends: set[Pos]
+
+
+def get_sides(xy: Pos) -> tuple[Pos, Pos, Pos, Pos]:
+ """Return the tile xy coordinates on the top left, top right, bottom left, and bottom right sides of given xy coordinates."""
+ cx, cy = xy
+ sides = []
+ for raw_dy in range(2):
+ dy = raw_dy * 2 - 1
+ ny = cy + dy
+ for raw_dx in range(2):
+ dx = raw_dx * 2 - 1
+ nx = cx + dx
+ sides.append((nx, ny))
+ tuple_sides = tuple(sides)
+ assert len(tuple_sides) == 4
+ return cast(tuple[Pos, Pos, Pos, Pos], tuple_sides)
+
+
+def pawn_modify(moves: tuple[T, ...], piece_type: u8) -> tuple[T, ...]:
+ """Return moves but remove invalid moves for pawns."""
+ assert (
+ len(moves) == 4
+ ), "Tuple size MUST be four for this to return valid results!"
+ if (
+ piece_type == 0
+ ): # If it's a white pawn, it can only move to top left and top right
+ return moves[:2]
+ if (
+ piece_type == 1
+ ): # If it's a black pawn, it can only move to bottom left anf bottom right
+ return moves[2:]
+ return moves
+
+
+@dataclass(slots=True)
+class State:
+ """Represents state of checkers game."""
+
+ size: tuple[int, int]
+ pieces: dict[Pos, int]
+ turn: bool = True # Black moves first
+
+ def __str__(self) -> str:
+ """Return text representation of game board state."""
+ map_ = {None: " ", 0: "-", 1: "+", 2: "O", 3: "X"}
+ w, h = self.size
+ lines = []
+ for y in range(h):
+ line = []
+ for x in range(w):
+ if (x + y + 1) % 2:
+ # line.append("_")
+ line.append(" ")
+ continue
+ line.append(map_[self.pieces.get((x, y))])
+ lines.append("".join(line))
+ # lines.append(" | ".join(line))
+ # lines.append("--+-"*(w-1)+"-")
+ return "\n".join(lines)
+
+ def calculate_actions(self, position: Pos) -> ActionSet:
+ """Return actions the piece at given position can make."""
+ if MANDATORY_CAPTURE:
+ exists = False
+ for start, _end in self.get_all_actions(self.pieces[position]):
+ if start == position:
+ exists = True
+ break
+ if not exists:
+ return ActionSet({}, (), set())
+ jumps = self.get_jumps(position)
+ moves: tuple[Pos, ...]
+ moves = () if MANDATORY_CAPTURE and jumps else self.get_moves(position)
+ ends = set(jumps)
+ ends.update(moves)
+ return ActionSet(jumps, moves, ends)
+
+ def piece_kinged(self, piece_pos: Pos, new_type: int) -> None:
+ """Piece kinged."""
+ # print(f'piece_kinged {piece = }')
+
+ def piece_moved(self, start_pos: Pos, end_pos: Pos) -> None:
+ """Piece moved from start_pos to end_pos."""
+
+ def piece_jumped(self, jumped_piece_pos: Pos) -> None:
+ """Piece has been jumped."""
+ # print(f'piece_jumped {position = }')
+
+ def preform_action(self, action: Action) -> Self:
+ """Return new state after performing action on self."""
+ from_pos, to_pos = action
+
+ pieces_copy = dict(self.pieces.items())
+
+ # Remove piece from it's start position
+ piece_type = pieces_copy.pop(from_pos)
+
+ # See if it's a jump
+ if to_pos not in self.get_moves(from_pos):
+ # Jumps are more complex to calculate and we need
+ # to know what pieces got jumped over
+ cur_x, cur_y = from_pos
+ for jumped_pos in self.get_jumps(from_pos)[to_pos]:
+ from_pos = (cur_x, cur_y)
+
+ # Remove jumped position from pieces in play
+ if jumped_pos in pieces_copy:
+ pieces_copy.pop(jumped_pos)
+ self.piece_jumped(jumped_pos)
+ # See if piece kinged
+ jumped_x, jumped_y = jumped_pos
+ # Rightshift 1 is more efficiant way to multiply by 2
+ cur_x += (jumped_x - cur_x) << 1
+ cur_y += (jumped_y - cur_y) << 1
+
+ self.piece_moved(from_pos, (cur_x, cur_y))
+
+ # Now that we know the current position, see if kinged
+ if self.does_piece_king(piece_type, (cur_x, cur_y)):
+ piece_type += 2
+ self.piece_kinged((cur_x, cur_y), piece_type)
+ else:
+ self.piece_moved(from_pos, to_pos)
+
+ # See if it kings and king it if so
+ if self.does_piece_king(piece_type, to_pos):
+ piece_type += 2
+ self.piece_kinged(to_pos, piece_type)
+
+ # Move piece to it's end position
+ pieces_copy[to_pos] = piece_type
+
+ # Swap turn
+ return self.__class__(
+ self.size,
+ pieces_copy,
+ not self.turn,
+ )
+
+ def get_tile_name(self, x: int, y: int) -> str:
+ """Return name of a given tile."""
+ return chr(65 + x) + str(self.size[1] - y)
+
+ @staticmethod
+ def action_from_points(start: Pos, end: Pos) -> Action:
+ """Return action from given start and end coordinates."""
+ # return Action(self.get_tile_name(*start), self.get_tile_name(*end))
+ return Action(start, end)
+
+ def get_turn(self) -> int:
+ """Return whose turn it is. 0 = red, 1 = black."""
+ return int(self.turn)
+
+ def valid_location(self, position: Pos) -> bool:
+ """Return if position is valid."""
+ x, y = position
+ w, h = self.size
+ return x >= 0 and y >= 0 and x < w and y < h
+
+ def does_piece_king(self, piece_type: int, position: Pos) -> bool:
+ """Return if piece needs to be kinged given it's type and position."""
+ _, y = position
+ _, h = self.size
+ return (piece_type == 0 and y == 0) or (piece_type == 1 and y == h - 1)
+
+ @staticmethod
+ def get_enemy(self_type: int) -> int:
+ """Return enemy pawn piece type."""
+ # If we are kinged, get a pawn version of ourselves.
+ # Take that plus one mod 2 to get the pawn of the enemy
+ return (self_type + 1) % 2
+
+ @staticmethod
+ def get_piece_types(self_type: int) -> tuple[int, int]:
+ """Return piece types of given piece type."""
+ # If we are kinged, get a pawn version of ourselves.
+ self_pawn = self_type % 2
+ return (self_pawn, self_pawn + 2)
+
+ def get_jumps(
+ self,
+ position: Pos,
+ piece_type: int | None = None,
+ _pieces: dict[Pos, int] | None = None,
+ _recursion: int = 0,
+ ) -> dict[Pos, list[Pos]]:
+ """Return valid jumps a piece can make.
+
+ position is a xy coordinate tuple pointing to a board position
+ that may or may not have a piece on it.
+ piece_type is the piece type at position. If not
+ given, position must point to a tile with a piece on it
+
+ Returns dictionary that maps end positions to
+ jumped pieces to get there
+ """
+ if piece_type is None:
+ piece_type = self.pieces[position]
+ if _pieces is None:
+ _pieces = self.pieces
+ _pieces = copy.deepcopy(_pieces)
+
+ enemy_pieces = self.get_piece_types(self.get_enemy(piece_type))
+
+ # Get the side coordinates of the tile and make them tuples so
+ # the scan later works properly.
+ sides = get_sides(position)
+ # Make a dictionary to find what direction a tile is in if you
+ # give it the tile.
+ # end position : jumped pieces
+
+ # Make a dictionary for the valid jumps and the pieces they jump
+ valid: dict[Pos, list[Pos]] = {}
+
+ valid_sides: tuple[tuple[int, Pos], ...]
+ if PAWN_JUMP_FORWARD_ONLY:
+ valid_sides = pawn_modify(
+ tuple(enumerate(sides)),
+ piece_type,
+ )
+ else:
+ valid_sides = tuple(enumerate(sides))
+
+ # For each side tile in the jumpable tiles for this type of piece,
+ for direction, side in valid_sides:
+ # Make sure side exists
+ if not self.valid_location(side):
+ continue
+ side_piece = _pieces.get(side)
+ # Side piece must be one of our enemy's pieces
+ if side_piece not in enemy_pieces:
+ continue
+ # Get the direction from the dictionary we made earlier
+ # Get the coordinates of the tile on the side of the main tile's
+ # side in the same direction as the main tile's side
+ side_side = get_sides(side)[direction]
+ # Make sure side exists
+ if not self.valid_location(side_side):
+ continue
+ side_side_piece = _pieces.get(side_side)
+ # If the side is open,
+ if side_side_piece is None:
+ # Add it the valid jumps dictionary and add the tile
+ # to the list of end tiles.
+ valid[side_side] = [side]
+
+ # Remove jumped piece from future calculations
+ _pieces.pop(side)
+
+ # For each end point tile in the list of end point tiles,
+ for end_tile in tuple(valid):
+ # Get the dictionary from the jumps you could make
+ # from that end tile
+ w, h = self.size
+ if _recursion + 1 > math.ceil((w**2 + h**2) ** 0.25):
+ break
+ # If the piece has made it to the opposite side,
+ piece_type_copy = piece_type
+ if self.does_piece_king(piece_type_copy, end_tile):
+ # King that piece
+ piece_type_copy += 2
+ _recursion = -1
+ add_valid = self.get_jumps(
+ end_tile,
+ piece_type_copy,
+ _pieces=_pieces,
+ _recursion=_recursion + 1,
+ )
+ # For each key in the new dictionary of valid tile's keys,
+ for end_pos, jumped_pieces in add_valid.items():
+ # If the key is not already existent in the list of
+ # valid destinations,
+ if end_pos not in valid:
+ # Add that destination to the dictionary and every
+ # tile you have to jump to get there.
+ no_duplicates = [
+ p for p in jumped_pieces if p not in valid[end_tile]
+ ]
+ valid[end_pos] = valid[end_tile] + no_duplicates
+
+ return valid
+
+ def get_moves(self, position: Pos) -> tuple[Pos, ...]:
+ """Return valid moves piece at position can make, not including jumps."""
+ piece_type = self.pieces[position]
+ # Get the side xy choords of the tile's xy pos,
+ # then modify results for pawns
+ moves = pawn_modify(get_sides(position), piece_type)
+ return tuple(
+ m
+ for m in filter(self.valid_location, moves)
+ if m not in self.pieces
+ )
+
+ @classmethod
+ def wrap_actions(
+ cls,
+ position: Pos,
+ calculate_ends: Callable[[Pos], Iterable[Pos]],
+ ) -> Generator[Action, None, None]:
+ """Yield end calculation function results as Actions."""
+ for end in calculate_ends(position):
+ yield cls.action_from_points(position, end)
+
+ def get_actions(self, position: Pos) -> Generator[Action, None, None]:
+ """Yield all moves and jumps the piece at position can make."""
+ ends = set(self.get_jumps(position))
+ if not (ends and MANDATORY_CAPTURE):
+ ends.update(self.get_moves(position))
+ for end in ends:
+ yield self.action_from_points(position, end)
+
+ def get_all_actions(self, player: int) -> Generator[Action, None, None]:
+ """Yield all actions for given player."""
+ player_pieces = {player, player + 2}
+ if not MANDATORY_CAPTURE:
+ for position, piece_type in self.pieces.items():
+ if piece_type not in player_pieces:
+ continue
+ yield from self.get_actions(position)
+ return
+ jumps_available = False
+ for position, piece_type in self.pieces.items():
+ if piece_type not in player_pieces:
+ continue
+ if not jumps_available:
+ for jump in self.wrap_actions(position, self.get_jumps):
+ yield jump
+ jumps_available = True
+ else:
+ yield from self.wrap_actions(position, self.get_jumps)
+ if not jumps_available:
+ for position, piece_type in self.pieces.items():
+ if piece_type not in player_pieces:
+ continue
+ yield from self.wrap_actions(position, self.get_moves)
+
+ def check_for_win(self) -> int | None:
+ """Return player number if they won else None."""
+ # For each of the two players,
+ for player in range(2):
+ # For each tile in the playable tiles,
+ has_move = False
+ for _ in self.get_all_actions(player):
+ has_move = True
+ # Player has at least one move, no need to continue
+ break
+ if not has_move and self.turn == bool(player):
+ # Continued without break, so player either has no moves
+ # or no possible moves, so their opponent wins
+ return (player + 1) % 2
+ return None
+
+ def can_player_select_piece(self, player: int, tile_pos: Pos) -> bool:
+ """Return True if player can select piece on given tile position."""
+ piece_at_pos = self.pieces.get(tile_pos)
+ if piece_at_pos is None:
+ return False
+ return (piece_at_pos % 2) == player
+
+ def get_pieces(self) -> tuple[tuple[Pos, int], ...]:
+ """Return all pieces."""
+ return tuple((pos, type_) for pos, type_ in self.pieces.items())
+
+
+def generate_pieces(
+ board_width: int,
+ board_height: int,
+ colors: int = 2,
+) -> dict[Pos, int]:
+ """Generate data about each piece."""
+ pieces: dict[Pos, int] = {}
+ # Get where pieces should be placed
+ z_to_1 = round(board_height / 3) # White
+ z_to_2 = (board_height - (z_to_1 * 2)) + z_to_1 # Black
+ # For each xy position in the area of where tiles should be,
+ for y in range(board_height):
+ # Reset the x pos to 0
+ for x in range(board_width):
+ # Get the color of that spot by adding x and y mod the number of different colors
+ color = (x + y + 1) % colors
+ # If a piece should be placed on that tile and the tile is not Red,
+ if (not color) and ((y <= z_to_1 - 1) or (y >= z_to_2)):
+ # Set the piece to White Pawn or Black Pawn depending on the current y pos
+ piece_type = int(y <= z_to_1)
+ pieces[x, y] = piece_type
+ return pieces
From 3f20f2d0fb68784e428da6327dbfb8756efe2d01 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Wed, 13 Nov 2024 19:29:41 -0600
Subject: [PATCH 02/67] Start converting to event based and state module
---
.pre-commit-config.yaml | 2 +-
computer_players/MiniMax_AI.py | 136 +++
computer_players/machine_client.py | 253 ++++
computer_players/minimax.py | 364 ++++++
src/azul/client.py | 139 ---
src/azul/component.py | 56 +
src/azul/game.py | 1791 ++++++++++++----------------
src/azul/server.py | 133 +--
src/azul/sprite.py | 22 +-
src/azul/state.py | 1405 +++++++++++++++-------
src/azul/tools.py | 44 +-
11 files changed, 2652 insertions(+), 1693 deletions(-)
create mode 100755 computer_players/MiniMax_AI.py
create mode 100644 computer_players/machine_client.py
create mode 100644 computer_players/minimax.py
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 77f196e..5da692c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -23,7 +23,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.7.3
+ rev: v0.7.4
hooks:
- id: ruff
types: [file]
diff --git a/computer_players/MiniMax_AI.py b/computer_players/MiniMax_AI.py
new file mode 100755
index 0000000..6bf8909
--- /dev/null
+++ b/computer_players/MiniMax_AI.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+# AI that plays checkers.
+
+"""Minimax Checkers AI."""
+
+from __future__ import annotations
+
+# Programmed by CoolCat467
+
+__title__ = "Minimax AI"
+__author__ = "CoolCat467"
+__version__ = "0.0.0"
+
+from typing import TYPE_CHECKING, TypeAlias, TypeVar
+
+##from machine_client import RemoteState, run_clients_in_local_servers_sync
+from minimax import Minimax, Player
+
+from azul.state import (
+ Phase,
+ SelectableDestinationTiles,
+ SelectableSourceTiles,
+ State,
+)
+
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+
+T = TypeVar("T")
+Action: TypeAlias = (
+ tuple[SelectableDestinationTiles, ...]
+ | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]]
+)
+
+# Player:
+# 0 = False = Person = MIN = 0, 2
+# 1 = True = AI (Us) = MAX = 1, 3
+
+
+class AzulMinimax(Minimax[State, Action]):
+ """Minimax Algorithm for Checkers."""
+
+ __slots__ = ()
+
+ @staticmethod
+ def value(state: State) -> int | float:
+ """Return value of given game state."""
+ # Real
+ real_state, max_player = state
+ if AzulMinimax.terminal(state):
+ winner, _score = real_state.get_win_order()[0]
+ if winner == max_player:
+ return 1
+ return -1
+ # Heuristic
+ min_ = 0
+ max_ = 0
+ for player_id, player_data in real_state.player_data.items():
+ score = player_data.get_end_of_game_score()
+ if player_id == max_player:
+ max_ += score
+ else:
+ min_ += score
+ # More max will make score higher,
+ # more min will make score lower
+ # Plus one in divisor makes so never / 0
+ return (max_ - min_) / (max_ + min_ + 1)
+
+ @staticmethod
+ def terminal(state: State) -> bool:
+ """Return if game state is terminal."""
+ real_state, _max_player = state
+ return real_state.current_phase == Phase.end
+
+ @staticmethod
+ def player(state: State) -> Player:
+ """Return Player enum from current state's turn."""
+ real_state, max_player = state
+ return (
+ Player.MAX if real_state.current_turn == max_player else Player.MIN
+ )
+
+ @staticmethod
+ def actions(state: State) -> Iterable[Action]:
+ """Return all actions that are able to be performed for the current player in the given state."""
+ real_state, _max_player = state
+ return tuple(real_state.yield_actions())
+ ## print(f'{len(actions) = }')
+
+ @staticmethod
+ def result(state: State, action: Action) -> State:
+ """Return new state after performing given action on given current state."""
+ real_state, max_player = state
+ return (real_state.preform_action(action), max_player)
+
+
+##class MinimaxPlayer(RemoteState):
+## """Minimax Player."""
+##
+## __slots__ = ()
+##
+## async def preform_turn(self) -> Action:
+## """Perform turn."""
+## print("preform_turn")
+## ##value, action = CheckersMinimax.adaptive_depth_minimax(
+## ## self.state, 4, 5
+## ##)
+## ##value, action = CheckersMinimax.minimax(self.state, 4)
+## value, action = CheckersMinimax.alphabeta(self.state, 4)
+## if action is None:
+## raise ValueError("action is None")
+## print(f"{value = }")
+## return action
+
+
+def run() -> None:
+ """Run MinimaxPlayer clients in local server."""
+ import random
+
+ random.seed(0)
+
+ state = (State.new_game(2), 0)
+
+ while not AzulMinimax.terminal(state):
+ action = AzulMinimax.alphabeta(state, 2)
+ print(f"{action.value = }")
+ state = AzulMinimax.result(state, action.action)
+ print(state)
+
+
+## run_clients_in_local_servers_sync(MinimaxPlayer)
+
+
+if __name__ == "__main__":
+ print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n")
+ run()
diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py
new file mode 100644
index 0000000..dac52f7
--- /dev/null
+++ b/computer_players/machine_client.py
@@ -0,0 +1,253 @@
+"""Machine Client - Checkers game client that can be controlled mechanically."""
+
+from __future__ import annotations
+
+__title__ = "Machine Client"
+__author__ = "CoolCat467"
+__version__ = "0.0.0"
+
+import sys
+from abc import ABCMeta, abstractmethod
+from contextlib import asynccontextmanager
+from typing import TYPE_CHECKING
+
+import trio
+from checkers.client import GameClient, read_advertisements
+from checkers.component import (
+ Component,
+ ComponentManager,
+ Event,
+ ExternalRaiseManager,
+)
+from checkers.state import Action, Pos, State
+
+if TYPE_CHECKING:
+ from collections.abc import AsyncGenerator
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
+
+# Player:
+# 0 = False = Person = MIN = 0, 2
+# 1 = True = AI (Us) = MAX = 1, 3
+
+
+class RemoteState(Component, metaclass=ABCMeta):
+ """Remote State.
+
+ Keeps track of game state and call preform_action when it's this clients
+ turn.
+ """
+
+ __slots__ = ("state", "pieces", "has_initial", "playing_as", "moves")
+
+ def __init__(self) -> None:
+ """Initialize remote state."""
+ super().__init__("remote_state")
+
+ self.state = State((8, 8), {})
+ self.has_initial = False
+ self.pieces: dict[Pos, int] = {}
+
+ self.playing_as = 1
+ self.moves = 0
+
+ def bind_handlers(self) -> None:
+ """Register game event handlers."""
+ self.register_handlers(
+ {
+ "game_action_complete": self.handle_action_complete,
+ "game_winner": self.handle_game_over,
+ "game_initial_config": self.handle_initial_config,
+ "game_playing_as": self.handle_playing_as,
+ "gameboard_create_piece": self.handle_create_piece,
+ },
+ )
+
+ async def preform_action(self, action: Action) -> None:
+ """Raise events to perform game action."""
+ await self.raise_event(
+ Event(
+ "gameboard_piece_clicked",
+ (
+ action.from_pos,
+ self.state.pieces[action.from_pos],
+ ),
+ ),
+ )
+ await self.raise_event(Event("gameboard_tile_clicked", action.to_pos))
+
+ @abstractmethod
+ async def preform_turn(self) -> Action:
+ """Perform turn, return action to perform."""
+
+ async def base_preform_turn(self) -> None:
+ """Perform turn."""
+ self.moves += 1
+ winner = self.state.check_for_win()
+ if winner is not None:
+ print("Terminal state, not performing turn")
+ value = ("Lost", "Won")[winner == self.playing_as]
+ print(f"{value} after {self.moves}")
+ return
+ action = await self.preform_turn()
+ await self.preform_action(action)
+
+ async def handle_action_complete(
+ self,
+ event: Event[tuple[Pos, Pos, int]],
+ ) -> None:
+ """Perform action on internal state and perform our turn if possible."""
+ from_pos, to_pos, turn = event.data
+ action = self.state.action_from_points(from_pos, to_pos)
+ self.state = self.state.preform_action(action)
+ ## print(f'{turn = }')
+ if turn == self.playing_as:
+ await self.base_preform_turn()
+
+ async def handle_create_piece(self, event: Event[tuple[Pos, int]]) -> None:
+ """Update internal pieces if we haven't had the initial setup event."""
+ if self.has_initial:
+ return
+ pos, type_ = event.data
+ self.pieces[pos] = type_
+
+ async def handle_playing_as(self, event: Event[int]) -> None:
+ """Handle playing as event."""
+ self.playing_as = event.data
+
+ async def handle_initial_config(
+ self,
+ event: Event[tuple[Pos, int]],
+ ) -> None:
+ """Set up initial state and perform our turn if possible."""
+ board_size, turn = event.data
+ self.state = State(board_size, self.pieces, bool(turn))
+ self.has_initial = True
+ if turn == self.playing_as:
+ await self.base_preform_turn()
+
+ async def handle_game_over(self, event: Event[int]) -> None:
+ """Raise network_stop event so we disconnect from server."""
+ self.has_initial = False
+ await self.raise_event(Event("network_stop", None))
+
+
+class MachineClient(ComponentManager):
+ """Manager that runs until client_disconnected event fires."""
+
+ __slots__ = ("running",)
+
+ def __init__(self, remote_state_class: type[RemoteState]) -> None:
+ """Initialize machine client."""
+ super().__init__("machine_client")
+
+ self.running = True
+
+ self.add_component(remote_state_class())
+
+ @asynccontextmanager
+ async def client_with_block(self) -> AsyncGenerator[GameClient, None]:
+ """Add client temporarily with `with` block, ensuring closure."""
+ async with GameClient("game_client") as client:
+ with self.temporary_component(client):
+ yield client
+
+ def bind_handlers(self) -> None:
+ """Register client event handlers."""
+ self.register_handlers(
+ {
+ "client_disconnected": self.handle_client_disconnected,
+ "client_connection_closed": self.handle_client_disconnected,
+ },
+ )
+
+ ## async def raise_event(self, event: Event) -> None:
+ ## """Raise event but also log it if not tick."""
+ ## if event.name not in {"tick"}:
+ ## print(f'{event = }')
+ ## return await super().raise_event(event)
+
+ async def handle_client_disconnected(self, event: Event[None]) -> None:
+ """Set self.running to false on network disconnect."""
+ self.running = False
+
+
+async def run_client(
+ host: str,
+ port: int,
+ remote_state_class: type[RemoteState],
+ connected: set[tuple[str, int]],
+) -> None:
+ """Run machine client and raise tick events."""
+ async with trio.open_nursery() as main_nursery:
+ event_manager = ExternalRaiseManager(
+ "checkers",
+ main_nursery,
+ "client",
+ )
+ client = MachineClient(remote_state_class)
+ with event_manager.temporary_component(client):
+ async with client.client_with_block():
+ await event_manager.raise_event(
+ Event("client_connect", (host, port)),
+ )
+ print(f"Connected to server {host}:{port}")
+ try:
+ while client.running: # noqa: ASYNC110
+ # Wait so backlog things happen
+ await trio.sleep(1)
+ except KeyboardInterrupt:
+ print("Shutting down client from keyboard interrupt.")
+ await event_manager.raise_event(
+ Event("network_stop", None),
+ )
+ print(f"Disconnected from server {host}:{port}")
+ client.unbind_components()
+ connected.remove((host, port))
+
+
+def run_client_sync(
+ host: str,
+ port: int,
+ remote_state_class: type[RemoteState],
+) -> None:
+ """Run client and connect to server at host:port."""
+ trio.run(run_client, host, port, remote_state_class, set())
+
+
+async def run_clients_in_local_servers(
+ remote_state_class: type[RemoteState],
+) -> None:
+ """Run clients in local servers."""
+ connected: set[tuple[str, int]] = set()
+ print("Watching for advertisements...\n(CTRL + C to quit)")
+ try:
+ async with trio.open_nursery(strict_exception_groups=True) as nursery:
+ while True:
+ advertisements = set(await read_advertisements())
+ servers = {server for _motd, server in advertisements}
+ servers -= connected
+ for server in servers:
+ connected.add(server)
+ nursery.start_soon(
+ run_client,
+ *server,
+ remote_state_class,
+ connected,
+ )
+ await trio.sleep(1)
+ except BaseExceptionGroup as exc:
+ for ex in exc.exceptions:
+ if isinstance(ex, KeyboardInterrupt):
+ print("Shutting down from keyboard interrupt.")
+ break
+ else:
+ raise
+
+
+def run_clients_in_local_servers_sync(
+ remote_state_class: type[RemoteState],
+) -> None:
+ """Run clients in local servers."""
+ trio.run(run_clients_in_local_servers, remote_state_class)
diff --git a/computer_players/minimax.py b/computer_players/minimax.py
new file mode 100644
index 0000000..c3a325f
--- /dev/null
+++ b/computer_players/minimax.py
@@ -0,0 +1,364 @@
+"""Minimax - Boilerplate code for Minimax AIs."""
+
+from __future__ import annotations
+
+# Programmed by CoolCat467
+
+__title__ = "Minimax"
+__author__ = "CoolCat467"
+__version__ = "0.0.0"
+
+import operator
+import random
+from abc import ABC, abstractmethod
+from enum import IntEnum, auto
+from math import inf as infinity
+from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar
+
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+
+
+class Player(IntEnum):
+ """Enum for player status."""
+
+ __slots__ = ()
+ MIN = auto()
+ MAX = auto()
+ CHANCE = auto()
+
+
+State = TypeVar("State")
+Action = TypeVar("Action")
+
+
+class MinimaxResult(NamedTuple, Generic[Action]):
+ """Minimax Result."""
+
+ value: int | float
+ action: Action | None
+
+
+class Minimax(ABC, Generic[State, Action]):
+ """Base class for Minimax AIs."""
+
+ __slots__ = ()
+
+ @classmethod
+ @abstractmethod
+ def value(cls, state: State) -> int | float:
+ """Return the value of a given game state."""
+
+ @classmethod
+ @abstractmethod
+ def terminal(cls, state: State) -> bool:
+ """Return if given game state is terminal."""
+
+ @classmethod
+ @abstractmethod
+ def player(cls, state: State) -> Player:
+ """Return player status given the state of the game.
+
+ Must return either Player.MIN or Player.MAX
+ """
+
+ @classmethod
+ @abstractmethod
+ def actions(cls, state: State) -> Iterable[Action]:
+ """Return a collection of all possible actions in a given game state."""
+
+ @classmethod
+ @abstractmethod
+ def result(cls, state: State, action: Action) -> State:
+ """Return new game state after performing action on given state."""
+
+ @classmethod
+ def minimax(
+ cls,
+ state: State,
+ depth: int | None = 5,
+ ) -> MinimaxResult[Action]:
+ """Return minimax result best action for a given state for the current player."""
+ if cls.terminal(state):
+ return MinimaxResult(cls.value(state), None)
+ if depth is not None and depth <= 0:
+ # Choose a random action
+ # No need for cryptographic secure random
+ return MinimaxResult(
+ cls.value(state),
+ random.choice(tuple(cls.actions(state))), # noqa: S311
+ )
+ next_down = None if depth is None else depth - 1
+
+ current_player = cls.player(state)
+ value: int | float
+ if current_player == Player.MAX:
+ value = -infinity
+ best = max
+ elif current_player == Player.MIN:
+ value = infinity
+ best = min
+ elif current_player == Player.CHANCE:
+ raise ValueError("CHANCE is not valid for regular minimax.")
+ else:
+ raise ValueError(f"Unexpected player type {current_player!r}")
+
+ best_action: Action | None = None
+ for action in cls.actions(state):
+ result = cls.minimax(cls.result(state, action), next_down)
+ new_value = best(value, result.value)
+ if new_value != value:
+ best_action = action
+ value = new_value
+ return MinimaxResult(value, best_action)
+
+ @classmethod
+ def alphabeta(
+ cls,
+ state: State,
+ depth: int | None = 5,
+ a: int | float = -infinity,
+ b: int | float = infinity,
+ ) -> MinimaxResult[Action]:
+ """Return minimax alphabeta pruning result best action for given current state."""
+ # print(f'alphabeta {depth = } {a = } {b = }')
+
+ if cls.terminal(state):
+ return MinimaxResult(cls.value(state), None)
+ if depth is not None and depth <= 0:
+ # Choose a random action
+ # No need for cryptographic secure random
+ return MinimaxResult(
+ cls.value(state),
+ random.choice(tuple(cls.actions(state))), # noqa: S311
+ )
+ next_down = None if depth is None else depth - 1
+
+ current_player = cls.player(state)
+ value: int | float
+ if current_player == Player.MAX:
+ value = -infinity
+ best = max
+ compare = operator.gt # greater than (>)
+ set_idx = 0
+ elif current_player == Player.MIN:
+ value = infinity
+ best = min
+ compare = operator.lt # less than (<)
+ set_idx = 1
+ elif current_player == Player.CHANCE:
+ raise ValueError("CHANCE is not valid for regular minimax.")
+ else:
+ raise ValueError(f"Unexpected player type {current_player!r}")
+
+ best_action: Action | None = None
+ for action in cls.actions(state):
+ result = cls.alphabeta(cls.result(state, action), next_down, a, b)
+ new_value = best(value, result.value)
+
+ if new_value != value:
+ best_action = action
+ value = new_value
+
+ if compare(new_value, (a, b)[set_idx ^ 1]):
+ # print("cutoff")
+ break # cutoff
+
+ alpha_beta_value = (a, b)[set_idx]
+ new_alpha_beta_value = best(alpha_beta_value, value)
+
+ if new_alpha_beta_value != alpha_beta_value:
+ # Set new best
+ alpha_beta_list = [a, b]
+ alpha_beta_list[set_idx] = new_alpha_beta_value
+ a, b = alpha_beta_list
+ return MinimaxResult(value, best_action)
+
+
+class Expectiminimax(ABC, Generic[State, Action]):
+ """Base class for Expectiminimax AIs."""
+
+ __slots__ = ()
+
+ LOWEST = -1
+ HIGHEST = 1
+
+ @classmethod
+ @abstractmethod
+ def value(cls, state: State) -> int | float:
+ """Return the value of a given game state."""
+
+ @classmethod
+ @abstractmethod
+ def terminal(cls, state: State) -> bool:
+ """Return if given game state is terminal."""
+
+ @classmethod
+ @abstractmethod
+ def player(cls, state: State) -> Player:
+ """Return player status given the state of the game.
+
+ Must return either Player.MIN or Player.MAX, or Player.CHANCE
+ if there is a random action.
+ """
+
+ @classmethod
+ @abstractmethod
+ def actions(cls, state: State) -> Iterable[Action]:
+ """Return a collection of all possible actions in a given game state."""
+
+ @classmethod
+ @abstractmethod
+ def result(cls, state: State, action: Action) -> State:
+ """Return new game state after performing action on given state."""
+
+ @classmethod
+ def probability(cls, action: Action) -> float:
+ """Return probability that given chance node action will happen.
+
+ Should be in range [0.0, 1.0] for 0% and 100% chance respectively.
+ """
+ return 1.0
+
+ @classmethod
+ def minimax(
+ cls,
+ state: State,
+ depth: int | None = 5,
+ ) -> MinimaxResult[Action]:
+ """Return minimax result best action for a given state for the current player."""
+ if cls.terminal(state):
+ return MinimaxResult(cls.value(state), None)
+ if depth is not None and depth <= 0:
+ # Choose a random action
+ # No need for cryptographic secure random
+ return MinimaxResult(
+ cls.value(state),
+ random.choice(tuple(cls.actions(state))), # noqa: S311
+ )
+ next_down = None if depth is None else depth - 1
+
+ current_player = cls.player(state)
+ value: int | float
+ if current_player == Player.MAX:
+ value = -infinity
+ best = max
+ elif current_player == Player.MIN:
+ value = infinity
+ best = min
+ elif current_player == Player.CHANCE:
+ value = 0
+ best = sum
+ else:
+ raise ValueError(f"Unexpected player type {current_player!r}")
+
+ best_action: Action | None = None
+ for action in cls.actions(state):
+ result = cls.minimax(cls.result(state, action), next_down)
+ result_value = result.value
+ if current_player == Player.CHANCE:
+ # Probability[action]
+ result_value *= cls.probability(action)
+ new_value = best(value, result_value)
+ if new_value != value and current_player != Player.CHANCE:
+ best_action = action
+ value = new_value
+ return MinimaxResult(value, best_action)
+
+ @classmethod
+ def alphabeta(
+ cls,
+ state: State,
+ depth: int | None = 5,
+ a: int | float = -infinity,
+ b: int | float = infinity,
+ ) -> MinimaxResult[Action]:
+ """Return minimax alphabeta pruning result best action for given current state."""
+ # print(f'alphabeta {depth = } {a = } {b = }')
+
+ if cls.terminal(state):
+ return MinimaxResult(cls.value(state), None)
+ if depth is not None and depth <= 0:
+ # Choose a random action
+ # No need for cryptographic secure random
+ return MinimaxResult(
+ cls.value(state),
+ random.choice(tuple(cls.actions(state))), # noqa: S311
+ )
+ next_down = None if depth is None else depth - 1
+
+ current_player = cls.player(state)
+ value: int | float
+ if current_player == Player.MAX:
+ value = -infinity
+ best = max
+ compare = operator.gt # greater than (>)
+ set_idx = 0
+ elif current_player == Player.MIN:
+ value = infinity
+ best = min
+ compare = operator.lt # less than (<)
+ set_idx = 1
+ elif current_player == Player.CHANCE:
+ value = 0
+ best = sum
+ else:
+ raise ValueError(f"Unexpected player type {current_player!r}")
+
+ actions = tuple(cls.actions(state))
+ successors = len(actions)
+ expect_a = successors * (a - cls.HIGHEST) + cls.HIGHEST
+ expect_b = successors * (b - cls.LOWEST) + cls.LOWEST
+
+ best_action: Action | None = None
+ for action in actions:
+ if current_player == Player.CHANCE:
+ # Limit child a, b to a valid range
+ ax = max(expect_a, cls.LOWEST)
+ bx = min(expect_b, cls.HIGHEST)
+ # Search the child with new cutoff values
+ result = cls.alphabeta(
+ cls.result(state, action),
+ next_down,
+ ax,
+ bx,
+ )
+ score = result.value
+ # Check for a, b cutoff conditions
+ if score <= expect_a:
+ return MinimaxResult(a, None)
+ if score >= expect_b:
+ return MinimaxResult(b, None)
+ value += score
+ # Adjust a, b for the next child
+ expect_a += cls.HIGHEST - score
+ expect_b += cls.LOWEST - score
+ continue
+
+ result = cls.alphabeta(cls.result(state, action), next_down, a, b)
+ new_value = best(value, result.value)
+
+ if new_value != value:
+ best_action = action
+ value = new_value
+
+ if compare(new_value, (a, b)[set_idx ^ 1]):
+ # print("cutoff")
+ break # cutoff
+
+ alpha_beta_value = (a, b)[set_idx]
+ new_alpha_beta_value = best(alpha_beta_value, value)
+
+ if new_alpha_beta_value != alpha_beta_value:
+ # Set new best
+ alpha_beta_list = [a, b]
+ alpha_beta_list[set_idx] = new_alpha_beta_value
+ a, b = alpha_beta_list
+ if current_player == Player.CHANCE:
+ # No cutoff occurred, return score
+ return MinimaxResult(value / successors, None)
+ return MinimaxResult(value, best_action)
+
+
+if __name__ == "__main__":
+ print(f"{__title__}\nProgrammed by {__author__}.\n")
diff --git a/src/azul/client.py b/src/azul/client.py
index 15e41e7..697101c 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -202,18 +202,7 @@ def bind_handlers(self) -> None:
self.register_handlers(
{
"server->callback_ping": self.read_callback_ping,
- "gameboard_piece_clicked": self.write_piece_click,
- "gameboard_tile_clicked": self.write_tile_click,
- "server->create_piece": self.read_create_piece,
- "server->select_piece": self.read_select_piece,
- "server->create_tile": self.read_create_tile,
- "server->delete_tile": self.read_delete_tile,
- "server->delete_piece_animation": self.read_delete_piece_animation,
- "server->update_piece_animation": self.read_update_piece_animation,
- "server->move_piece_animation": self.read_move_piece_animation,
- "server->animation_state": self.read_animation_state,
"server->game_over": self.read_game_over,
- "server->action_complete": self.read_action_complete,
"server->initial_config": self.read_initial_config,
"server->playing_as": self.read_playing_as,
"server->encryption_request": self.read_encryption_request,
@@ -348,118 +337,6 @@ async def read_callback_ping(self, event: Event[bytearray]) -> None:
Event("callback_ping", difference),
)
- async def read_create_piece(self, event: Event[bytearray]) -> None:
- """Read create_piece event from server."""
- buffer = Buffer(event.data)
-
- piece_pos = read_position(buffer)
- piece_type: u8 = buffer.read_value(StructFormat.UBYTE)
-
- await self.raise_event(
- Event("gameboard_create_piece", (piece_pos, piece_type)),
- )
-
- async def read_select_piece(self, event: Event[bytearray]) -> None:
- """Read create_piece event from server."""
- buffer = Buffer(event.data)
-
- piece_pos = read_position(buffer)
- outline_value = buffer.read_value(StructFormat.BOOL)
-
- await self.raise_event(
- Event("gameboard_select_piece", (piece_pos, outline_value)),
- )
-
- async def read_create_tile(self, event: Event[bytearray]) -> None:
- """Read create_tile event from server."""
- buffer = Buffer(event.data)
-
- tile_pos = read_position(buffer)
-
- await self.raise_event(Event("gameboard_create_tile", tile_pos))
-
- async def read_delete_tile(self, event: Event[bytearray]) -> None:
- """Read delete_tile event from server."""
- buffer = Buffer(event.data)
-
- tile_pos = read_position(buffer)
-
- await self.raise_event(Event("gameboard_delete_tile", tile_pos))
-
- async def write_piece_click(self, event: Event[tuple[Pos, int]]) -> None:
- """Write piece click event to server."""
- if self.not_connected:
- return
- piece_position, _piece_type = event.data
-
- buffer = Buffer()
- write_position(buffer, piece_position)
- # buffer.write_value(StructFormat.UINT, piece_type)
-
- await self.write_event(Event("select_piece->server", buffer))
-
- async def write_tile_click(self, event: Event[Pos]) -> None:
- """Write tile click event to server."""
- if self.not_connected:
- return
- tile_position = event.data
-
- buffer = Buffer()
- write_position(buffer, tile_position)
-
- await self.write_event(Event("select_tile->server", buffer))
-
- async def read_delete_piece_animation(
- self,
- event: Event[bytearray],
- ) -> None:
- """Read delete_piece_animation event from server."""
- buffer = Buffer(event.data)
-
- tile_pos = read_position(buffer)
-
- await self.raise_event(
- Event("gameboard_delete_piece_animation", tile_pos),
- )
-
- async def read_update_piece_animation(
- self,
- event: Event[bytearray],
- ) -> None:
- """Read update_piece_animation event from server."""
- buffer = Buffer(event.data)
-
- piece_pos = read_position(buffer)
- piece_type: u8 = buffer.read_value(StructFormat.UBYTE)
-
- await self.raise_event(
- Event("gameboard_update_piece_animation", (piece_pos, piece_type)),
- )
-
- async def read_move_piece_animation(self, event: Event[bytearray]) -> None:
- """Read move_piece_animation event from server."""
- buffer = Buffer(event.data)
-
- piece_current_pos = read_position(buffer)
- piece_new_pos = read_position(buffer)
-
- await self.raise_event(
- Event(
- "gameboard_move_piece_animation",
- (piece_current_pos, piece_new_pos),
- ),
- )
-
- async def read_animation_state(self, event: Event[bytearray]) -> None:
- """Read animation_state event from server."""
- buffer = Buffer(event.data)
-
- animation_state = buffer.read_value(StructFormat.BOOL)
-
- await self.raise_event(
- Event("gameboard_animation_state", animation_state),
- )
-
async def read_game_over(self, event: Event[bytearray]) -> None:
"""Read update_piece event from server."""
buffer = Buffer(event.data)
@@ -469,22 +346,6 @@ async def read_game_over(self, event: Event[bytearray]) -> None:
await self.raise_event(Event("game_winner", winner))
self.running = False
- async def read_action_complete(self, event: Event[bytearray]) -> None:
- """Read action_complete event from server.
-
- Sent when last action from client is done, great for AIs.
- As of writing, not used for main client.
- """
- buffer = Buffer(event.data)
-
- from_pos = read_position(buffer)
- to_pos = read_position(buffer)
- current_turn: u8 = buffer.read_value(StructFormat.UBYTE)
-
- await self.raise_event(
- Event("game_action_complete", (from_pos, to_pos, current_turn)),
- )
-
async def read_initial_config(self, event: Event[bytearray]) -> None:
"""Read initial_config event from server."""
buffer = Buffer(event.data)
diff --git a/src/azul/component.py b/src/azul/component.py
index 3e6865b..487db3a 100644
--- a/src/azul/component.py
+++ b/src/azul/component.py
@@ -145,6 +145,25 @@ def has_handler(self, event_name: str) -> bool:
"""
return self.manager.has_handler(event_name)
+ def unregister_handler(
+ self,
+ event_name: str,
+ handler_coro: Callable[[Event[Any]], Awaitable[None]],
+ ) -> None:
+ """Unregister a handler function for event_name.
+
+ Raises ValueError if no component with given name is registered.
+ """
+ return self.manager.unregister_handler(
+ event_name,
+ handler_coro,
+ self.name,
+ )
+
+ def unregister_handler_type(self, event_name: str) -> bool:
+ """Unregister all event handlers for a given event type."""
+ return self.manager.unregister_handler_type(event_name)
+
async def raise_event(self, event: Event[Any]) -> None:
"""Raise event for bound manager.
@@ -247,6 +266,43 @@ def register_component_handler(
self.__event_handlers[event_name] = set()
self.__event_handlers[event_name].add((handler_coro, component_name))
+ def unregister_handler(
+ self,
+ event_name: str,
+ handler_coro: Callable[[Event[Any]], Awaitable[None]],
+ component_name: object,
+ ) -> None:
+ """Unregister a handler function for event_name.
+
+ Raises ValueError if no component with given name is registered.
+ """
+ if (
+ component_name != self.name
+ and component_name not in self.__components
+ ):
+ raise ValueError(
+ f"Component named {component_name!r} is not registered!",
+ )
+
+ if event_name not in self.__event_handlers:
+ return
+
+ handler_tuple = (handler_coro, component_name)
+ if handler_tuple in self.__event_handlers[event_name]:
+ self.__event_handlers[event_name].remove(handler_tuple)
+
+ # If the event_name no longer has any handlers, remove it
+ if not self.__event_handlers[event_name]:
+ del self.__event_handlers[event_name]
+
+ def unregister_handler_type(
+ self,
+ event_name: str,
+ ) -> None:
+ """Unregister all event handlers for a given event type."""
+ if event_name in self.__event_handlers:
+ del self.__event_handlers[event_name]
+
def has_handler(self, event_name: str) -> bool:
"""Return if there are event handlers registered for a given event."""
return bool(self.__event_handlers.get(event_name))
diff --git a/src/azul/game.py b/src/azul/game.py
index 6ecb4c3..29512a5 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -23,62 +23,63 @@
__author__ = "CoolCat467"
__version__ = "2.0.0"
+import contextlib
import importlib
import math
import operator
import os
import random
-import traceback
+import sys
import time
-from collections import Counter, deque
+import traceback
+from collections import Counter
from functools import lru_cache, wraps
from pathlib import Path
-import trio
-from typing import TYPE_CHECKING, Final, NamedTuple, TypeVar, cast
+from typing import TYPE_CHECKING, Final, TypeVar
import pygame
-from pygame.color import Color
+import trio
from numpy import array, int8
+from pygame.color import Color
from pygame.locals import (
- KEYDOWN,
- KEYUP,
K_ESCAPE,
+ KEYUP,
QUIT,
RESIZABLE,
SRCALPHA,
USEREVENT,
- VIDEORESIZE,
WINDOWRESIZED,
)
from pygame.rect import Rect
+from azul import element_list, objects, sprite
+from azul.async_clock import Clock
+from azul.client import GameClient, read_advertisements
+from azul.component import (
+ ComponentManager,
+ Event,
+ ExternalRaiseManager,
+)
+from azul.network_shared import DEFAULT_PORT, find_ip
+from azul.server import GameServer
+from azul.sound import SoundData, play_sound as base_play_sound
+from azul.statemachine import AsyncState
from azul.tools import (
- floor_line_subtract_generator,
- gen_random_proper_seq,
lerp_color,
- randomize,
saturate,
- sort_tiles,
)
from azul.vector import Vector2
-from azul import sprite
-from azul import objects
-from azul.component import Component, Event, ExternalRaiseManager, ComponentManager
-from azul import errorbox
-from azul.async_clock import Clock
-from azul.server import GameServer
-from azul.sound import SoundData, play_sound as base_play_sound
-from azul import element_list
-from azul.client import GameClient, read_advertisements
-from azul.statemachine import AsyncState
-from azul.network_shared import DEFAULT_PORT, find_ip
-from enum import IntEnum, auto
-import sys
if TYPE_CHECKING:
- from collections.abc import Callable, Generator, Iterable, Sequence, Awaitable
+ from collections.abc import (
+ Awaitable,
+ Callable,
+ Generator,
+ Iterable,
+ Sequence,
+ )
- from typing_extensions import TypeVarTuple, Unpack
+ from typing_extensions import TypeVarTuple
P = TypeVarTuple("P")
@@ -130,19 +131,9 @@
("&", ORANGE),
("1", BLUE),
)
-class Tiles(IntEnum):
- blank = -6
- fake_cyan = -5
- fake_black = -4
- fake_red = -3
- fake_yellow = -2
- fake_blue = -1
- blue = 0
- yellow = auto()
- red = auto()
- black = auto()
- cyan = auto()
- one = auto()
+
+
+from azul.state import Tile
TILESIZE = 15
@@ -157,7 +148,7 @@ class Tiles(IntEnum):
GREYSHIFT = 0.75 # 0.65
# Font
-FONT: Final = FONT_FOLDER / "VeraSerif.ttf"#"RuneScape-UF-Regular.ttf"
+FONT: Final = FONT_FOLDER / "VeraSerif.ttf" # "RuneScape-UF-Regular.ttf"
SCOREFONTSIZE = 30
BUTTONFONTSIZE = 60
@@ -278,7 +269,7 @@ def get_tile_color(
) -> tuple[int, int, int] | tuple[tuple[int, int, int], tuple[int, int, int]]:
"""Return the color a given tile should be."""
if tile_color < 0:
- if tile_color == -6:
+ if tile_color == Tile.blank:
return GREY
color = tile_colors[abs(tile_color + 1)]
assert len(color) == 3
@@ -296,7 +287,7 @@ def get_tile_symbol_and_color(
) -> tuple[str, tuple[int, int, int]]:
"""Return the color a given tile should be."""
if tile_color < 0:
- if tile_color == -6:
+ if tile_color == Tile.blank:
return " ", GREY
symbol, scolor = TILESYMBOLS[abs(tile_color + 1)]
r, g, b = lerp_color(scolor, GREY, greyshift)
@@ -348,7 +339,6 @@ def add_symbol_to_tile_surf(
# surf.blit(symbolsurf, (0, 0))
-@lru_cache
def get_tile_image(
tile_color: int,
tilesize: int,
@@ -378,22 +368,8 @@ def get_tile_image(
return surf
-def set_alpha(
- surface: pygame.surface.Surface,
- alpha: int,
-) -> pygame.surface.Surface:
- """Return a surface by replacing the alpha channel of it with given alpha value, preserve color."""
- surface = surface.copy().convert_alpha()
- w, h = surface.get_size()
- for y in range(h):
- for x in range(w):
- r, g, b = cast("tuple[int, int, int]", surface.get_at((x, y))[:3])
- surface.set_at((x, y), pygame.Color(r, g, b, alpha))
- return surface
-
-
def get_tile_container_image(
- wh: tuple[int, int],
+ width_height: tuple[int, int],
back: (
pygame.color.Color
| int
@@ -405,7 +381,7 @@ def get_tile_container_image(
),
) -> pygame.surface.Surface:
"""Return a tile container image from a width and a height and a background color, and use a game's cache to help."""
- image = pygame.surface.Surface(wh, flags=SRCALPHA)
+ image = pygame.surface.Surface(width_height, flags=SRCALPHA)
if back is not None:
image.fill(back)
else:
@@ -413,7 +389,6 @@ def get_tile_container_image(
return image
-
class ObjectHandler:
"""ObjectHandler class, meant to be used for other classes."""
@@ -575,7 +550,6 @@ def __del__(self) -> None:
self.rm_star()
-
class MultipartObject(ObjectHandler):
"""Thing that is both an Object and an ObjectHandler, and is meant to be an Object made up of multiple Objects."""
@@ -637,77 +611,100 @@ def __del__(self) -> None:
class TileRenderer(sprite.Sprite):
"""Base class for all objects that need to render tiles."""
- __slots__ = ("background", "tile_seperation")
+ __slots__ = ("background", "tile_separation")
greyshift = GREYSHIFT
tile_size = TILESIZE
def __init__(
self,
name: str,
- tile_seperation: int | None = None,
+ tile_separation: int | None = None,
background: tuple[int, int, int] | None = TILEDEFAULT,
) -> None:
"""Initialize renderer."""
super().__init__(name)
- if tile_seperation is None:
- self.tile_seperation = self.tile_size / 3.75
+ if tile_separation is None:
+ self.tile_separation = self.tile_size / 3.75
else:
- self.tile_seperation = tile_seperation
+ self.tile_separation = tile_separation
self.background = background
- def get_rect(self) -> Rect:
- """Return a Rect object representing this row's area."""
- wh = (
- self.wh[0] - self.tile_seperation * 2,
- self.wh[1] - self.tile_seperation * 2,
- )
- location = self.location[0] - wh[0] / 2, self.location[1] - wh[1] / 2
- return Rect(location, wh)
-
def clear_image(self, tile_dimensions: tuple[int, int]) -> None:
- """Reset self.image using tile_dimensions tuple and fills with self.background. Also updates self.wh."""
- tw, th = tile_dimensions
- tile_full = self.tile_size + self.tile_seperation
+ """Reset self.image using tile_dimensions tuple and fills with self.background. Also updates self.width_height."""
+ tile_width, tile_height = tile_dimensions
+ tile_full = self.tile_size + self.tile_separation
self.image = get_tile_container_image(
(
- round(tw * tile_full + self.tile_seperation),
- round(th * tile_full + self.tile_seperation),
+ round(tile_width * tile_full + self.tile_separation),
+ round(tile_height * tile_full + self.tile_separation),
),
- self.background
+ self.background,
)
def blit_tile(
self,
tile_color: int,
tile_location: tuple[int, int],
+ offset: tuple[int, int] | None = None,
) -> None:
"""Blit the surface of a given tile object onto self.image at given tile location. It is assumed that all tile locations are xy tuples."""
x, y = tile_location
- surf = get_tile_image(tile_object, self.tile_size, self.greyshift)
+ if offset is None:
+ ox, oy = 0, 0
+ else:
+ ox, oy = offset
+
+ ox += self.tile_separation
+ oy += self.tile_separation
+
+ surf = get_tile_image(tile_color, self.tile_size, self.greyshift)
assert self.image is not None
+
+ tile_full = self.tile_size + self.tile_separation
+
self.image.blit(
surf,
(
- round(x * self.tile_full + self.tile_seperation),
- round(y * self.tile_full + self.tile_seperation),
+ round(x * tile_full + ox),
+ round(y * tile_full + oy),
),
)
def to_image_surface_location(
self,
screen_location: tuple[int, int] | Vector2,
- ) -> tuple[int, int]:
- """Return the location a screen location would be at on the objects image. Can return invalid data."""
- # Get zero zero in image locations
- zx, zy = self.rect.topleft
- sx, sy = screen_location # Screen x and y
- # Location with respect to image dimensions
- return (
- int(sx) - zx,
- int(sy) - zy,
- )
+ ) -> Vector2:
+ """Return screen location with respect to top left of image."""
+ return Vector2.from_points(self.rect.topleft, screen_location)
+
+ def get_tile_point(
+ self,
+ screen_location: tuple[int, int] | Vector2,
+ ) -> tuple[int, int] | None:
+ """Return the xy choordinates of which tile intersects given a point or None."""
+ # Can't get tile if screen location doesn't intersect our hitbox!
+ if not self.is_selected(screen_location):
+ return None
+
+ # Find out where screen point is in image locations
+ # board x and y
+ surface_pos = self.to_image_surface_location(screen_location)
+ # Subtract separation boarder offset
+ surface_pos -= (self.tile_separation, self.tile_separation)
+
+ tile_full = self.tile_size + self.tile_separation
+
+ # Get tile position and offset into that tile
+ tile_position, offset = divmod(surface_pos, tile_full)
+ for value in offset:
+ # If in separation region, not selected
+ if value > self.tile_size:
+ return None
+ # Otherwise, not in separation region, so we should be good
+ return tile_position
+
## def screen_size_update(self) -> None:
## """Handle screensize is changes."""
@@ -725,7 +722,14 @@ def to_image_surface_location(
class Cursor(TileRenderer):
- """Cursor Object."""
+ """Cursor TileRenderer.
+
+ Registers following event handlers:
+ - cursor_drag
+ - cursor_reached_destination
+ - cursor_set_destination
+ - cursor_set_location
+ """
__slots__ = ("tiles",)
greyshift = GREYSHIFT
@@ -733,16 +737,87 @@ class Cursor(TileRenderer):
def __init__(self) -> None:
"""Initialize cursor with a game it belongs to."""
super().__init__("Cursor", background=None)
+ self.update_location_on_resize = True
+ self.add_components(
+ (
+ sprite.MovementComponent(speed=800),
+ sprite.TargetingComponent("cursor_reached_destination"),
+ ),
+ )
+
+ # Stored in reverse render order
self.tiles: list[int] = []
def update_image(self) -> None:
"""Update self.image."""
- self.clear_image((len(self.tiles), 1))
+ tile_count = len(self.tiles)
+ self.clear_image((tile_count, 1))
- for x in range(len(self.tiles)):
- self.blit_tile(self.tiles[x], (x, 0))
- self.dirty = 1
+ # Render in reverse order so keeping number one on end is easier
+ for x in range(tile_count):
+ self.blit_tile(self.tiles[tile_count - x - 1], (x, 0))
+ if tile_count:
+ self.dirty = 1
+ self.visible = bool(tile_count)
+
+ def bind_handlers(self):
+ """Register handlers."""
+ self.register_handlers(
+ {
+ "cursor_drag": self.handle_cursor_drag,
+ "cursor_reached_destination": self.handle_cursor_reached_destination,
+ "cursor_set_destination": self.handle_cursor_set_destination,
+ "cursor_set_location": self.handle_cursor_set_location,
+ },
+ )
+
+ async def handle_cursor_drag(self, event: Event[Iterable[int]]) -> None:
+ """Drag one or more tiles."""
+ await trio.lowlevel.checkpoint()
+ for tile_color in event.data:
+ if tile_color == Tile.one:
+ self.tiles.insert(0, tile_color)
+ else:
+ self.tiles.append(tile_color)
+ self.update_image()
+
+ async def handle_cursor_reached_destination(
+ self,
+ event: Event[None],
+ ) -> None:
+ """Stop ticking."""
+ self.unregister_handler_type("tick")
+ await trio.lowlevel.checkpoint()
+
+ def move_to_front(self) -> None:
+ """Move this sprite to front."""
+ group: sprite.LayeredDirty = self.groups()[-1]
+ group.move_to_front(self)
+
+ async def handle_cursor_set_destination(
+ self,
+ event: Event[tuple[int, int]],
+ ) -> None:
+ """Start moving towards new destination."""
+ targeting: sprite.TargetingComponent = self.get_component("targeting")
+ targeting.destination = event.data
+ if not self.has_handler("tick"):
+ self.register_handler(
+ "tick",
+ targeting.move_destination_time_ticks,
+ )
+ self.move_to_front()
+ await trio.lowlevel.checkpoint()
+
+ async def handle_cursor_set_location(
+ self,
+ event: Event[tuple[int, int]],
+ ) -> None:
+ """Set location to event data."""
+ self.move_to_front()
+ self.location = event.data
+ await trio.lowlevel.checkpoint()
def get_held_count(self) -> int:
"""Return the number of held tiles."""
@@ -752,113 +827,28 @@ def is_holding(self) -> bool:
"""Return True if the mouse is dragging something."""
return len(self.tiles) > 0
- def get_held_info(
- self,
- ) -> tuple[int, ...]:
+ def get_held_info(self) -> tuple[int, ...]:
"""Return tuple of currently held tiles."""
- return tuple(self.tiles)
-
- def process(self, time_passed: float) -> None:
- """Process cursor."""
- x, y = pygame.mouse.get_pos()
- x = saturate(x, 0, SCREEN_SIZE[0])
- y = saturate(y, 0, SCREEN_SIZE[1])
- self.location = (x, y)
-
- def force_hold(self, tiles: Iterable[Tile]) -> None:
- """Pretty much it's drag but with no constraints."""
- for tile in tiles:
- if tile.color == Tiles.one:
- self.holding_number_one = True
- self.tiles.append(tile)
- else:
- self.tiles.appendleft(tile)
- self.image_update = True
-
- def drag(self, tiles: Iterable[Tile]) -> None:
- """Drag one or more tiles, as long as it's a list."""
- for tile in tiles:
- if tile is not None and tile.color == Tiles.one:
- self.holding_number_one = True
- self.tiles.append(tile)
- else:
- self.tiles.appendleft(tile)
- self.image_update = True
+ return tuple(reversed(self.tiles))
def drop(
self,
number: int | None = None,
- allow_number_one_tile: bool = False,
- ) -> list[Tile]:
- """Return all of the tiles the Cursor is carrying."""
- if self.is_holding(allow_number_one_tile):
- if number is None:
- number = self.get_held_count(allow_number_one_tile)
- else:
- number = saturate(
- number,
- 0,
- self.get_held_count(allow_number_one_tile),
- )
-
- tiles = []
- for tile in (self.tiles.popleft() for i in range(number)):
- if tile.color == Tiles.one and not allow_number_one_tile:
- self.tiles.append(tile)
- continue
- tiles.append(tile)
- self.image_update = True
-
- self.holding_number_one = Tiles.one in {
- tile.color for tile in self.tiles
- }
- return tiles
- return []
-
- def drop_one_tile(self) -> Tile | None:
- """If holding the number one tile, drop it (returns it)."""
- if self.holding_number_one:
- not_number_one_tile = self.drop(None, False)
- one = self.drop(1, True)
- self.drag(not_number_one_tile)
- self.holding_number_one = False
- return one[0]
- return None
-
-
-G = TypeVar("G", bound="Grid")
-
-
-def gsc_bound_index(
- bounds_failure_return: T,
-) -> Callable[
- [Callable[[G, tuple[int, int], *P], RT]],
- Callable[[G, tuple[int, int], *P], RT | T],
-]:
- """Return a decorator for any grid or grid subclass that will keep index positions within bounds."""
-
- def gsc_bounds_keeper(
- function: Callable[[G, tuple[int, int], *P], RT],
- ) -> Callable[[G, tuple[int, int], *P], RT | T]:
- """Grid or Grid Subclass Decorator that keeps index positions within bounds, as long as index is first argument after self arg."""
-
- @wraps(function)
- def keep_within_bounds(
- self: G,
- index: tuple[int, int],
- *args: Unpack[P],
- ) -> RT | T:
- """Ensure a index position tuple is valid."""
- x, y = index
- if x < 0 or x >= self.size[0]:
- return bounds_failure_return
- if y < 0 or y >= self.size[1]:
- return bounds_failure_return
- return function(self, index, *args)
-
- return keep_within_bounds
+ ) -> tuple[int, ...]:
+ """Pop and return tiles the Cursor is carrying.
- return gsc_bounds_keeper
+ If number is None, pops all tiles, otherwise only pops given count.
+ """
+ if number is None:
+ tiles_copy = self.get_held_info()
+ self.tiles.clear()
+ self.update_image()
+ return tiles_copy
+ tiles: list[int] = []
+ for _ in range(number):
+ tiles.append(self.tiles.pop())
+ self.update_image()
+ return tuple(tiles)
class Grid(TileRenderer):
@@ -868,303 +858,233 @@ class Grid(TileRenderer):
def __init__(
self,
+ name: str,
size: tuple[int, int],
- game: Game,
- tile_seperation: int | None = None,
+ tile_separation: int | None = None,
background: tuple[int, int, int] | None = TILEDEFAULT,
) -> None:
"""Grid Objects require a size and game at least."""
- super().__init__("Grid", game, tile_seperation, background)
+ super().__init__(name, tile_separation, background)
self.size = size
self.data = array(
- [-6 for i in range(int(self.size[0] * self.size[1]))],
+ [Tile.blank for i in range(int(self.size[0] * self.size[1]))],
int8,
).reshape(self.size)
+ def get_tile(self, xy: tuple[int, int]) -> int:
+ """Return tile color at given index."""
+ x, y = xy
+ return int(self.data[y, x])
+
def update_image(self) -> None:
"""Update self.image."""
self.clear_image(self.size)
- for y in range(self.size[1]):
- for x in range(self.size[0]):
- self.blit_tile(Tile(self.data[y, x]), (x, y))
+ width, height = self.size
- def get_tile_point(
- self,
- screen_location: tuple[int, int] | Vector2,
- ) -> tuple[int, int] | None:
- """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections."""
- # Can't get tile if screen location doesn't intersect our hitbox!
- if not self.is_selected(screen_location):
- return None
- # Otherwise, find out where screen point is in image locations
- # board x and y
- bx, by = self.to_image_surface_location(screen_location)
- # Finally, return the full divides (no decimals) of xy location by self.tile_full.
- return int(bx // self.tile_full), int(by // self.tile_full)
+ for y in range(height):
+ for x in range(width):
+ pos = (x, y)
+ self.blit_tile(self.get_tile(pos), pos)
- @gsc_bound_index(None)
- def place_tile(self, xy: tuple[int, int], tile: Tile) -> bool:
- """Place a Tile Object if permitted to do so. Return True if success."""
- x, y = xy
- if self.data[y, x] < 0:
- self.data[y, x] = tile.color
- del tile
- self.image_update = True
- return True
- return False
+ def fake_tile_exists(self, xy: tuple[int, int]) -> bool:
+ """Return if tile at given position is a fake tile."""
+ return self.get_tile(xy) < 0
- @gsc_bound_index(None)
- def get_tile(self, xy: tuple[int, int], replace: int = -6) -> Tile | None:
- """Return a Tile Object from a given position in the grid if permitted. Return None on failure."""
+ def place_tile(self, xy: tuple[int, int], tile_color: int) -> bool:
+ """Place tile at given position."""
x, y = xy
- tile_color = int(self.data[y, x])
- if tile_color < 0:
- return None
- self.data[y, x] = replace
- self.image_update = True
- return Tile(tile_color)
+ self.data[y, x] = tile_color
+ self.update_image()
- @gsc_bound_index(None)
- def get_info(self, xy: tuple[int, int]) -> Tile:
- """Return the Tile Object at a given position without deleting it from the Grid."""
- x, y = xy
- color = int(self.data[y, x])
- return Tile(color)
+ def pop_tile(self, xy: tuple[int, int], replace: int = Tile.blank) -> int:
+ """Return popped tile from given position in the grid."""
+ tile_color = self.get_tile(xy)
+ self.place_tile(xy, replace)
+ return tile_color
- def get_colors(self) -> list[int]:
- """Return a list of the colors of tiles within self."""
+ def get_colors(self) -> set[int]:
+ """Return a set of the colors of tiles within self."""
colors = set()
- for y in range(self.size[1]):
- for x in range(self.size[0]):
- info_color = int(self.data[y, x])
- assert info_color is not None
- colors.add(info_color)
- return list(colors)
+ width, height = self.size
+ for y in range(height):
+ for x in range(width):
+ colors.add(self.get_tile((x, y)))
+ return colors
- def is_empty(self, empty_color: int = -6) -> bool:
+ def is_empty(self, empty_color: int = Tile.blank) -> bool:
"""Return True if Grid is empty (all tiles are empty_color)."""
colors = self.get_colors()
- # Colors should only be [-6] if empty
- return colors == [empty_color]
-
- def __del__(self) -> None:
- """Delete data."""
- super().__del__()
- del self.data
+ return len(colors) == 1 and colors.pop() == empty_color
class Board(Grid):
"""Represents the board in the Game."""
- __slots__ = ("additions", "player", "variant_play", "wall_tiling")
- bcolor = ORANGE
+ __slots__ = ("additions", "variant_play", "wall_tiling")
- def __init__(self, player: Player, variant_play: bool = False) -> None:
+ def __init__(self, variant_play: bool = False) -> None:
"""Initialize player's board."""
- super().__init__((5, 5), player.game, background=self.bcolor)
- self.name = "Board"
- self.player = player
+ super().__init__("Board", (5, 5), background=ORANGE)
self.variant_play = variant_play
- self.additions: dict[int, Tile | int | None] = {}
+ self.additions: dict[int, int | None] = {}
self.wall_tiling = False
+ if not variant_play:
+ self.set_colors()
+ else:
+ self.update_image()
+ self.visible = True
+
def __repr__(self) -> str:
"""Return representation of self."""
- return (
- f"{self.__class__.__name__}({self.player!r}, {self.variant_play})"
- )
+ return f"{self.__class__.__name__}({self.variant_play})"
- def set_colors(self, keep_read: bool = True) -> None:
+ def set_colors(self, keep_real: bool = True) -> None:
"""Reset tile colors."""
- for y in range(self.size[1]):
- for x in range(self.size[0]):
- if not keep_read or self.data[y, x] < 0:
- self.data[y, x] = -(
- (self.size[1] - y + x) % REGTILECOUNT + 1
- )
-
- # print(self.data[y, x], end=' ')
- # print()
- # print('-'*10)
-
- def get_row(self, index: int) -> Generator[Tile, None, None]:
+ width, height = self.size
+ for y in range(height):
+ for x in range(width):
+ if not keep_real or self.fake_tile_exists((x, y)):
+ color = -((height - y + x) % REGTILECOUNT + 1)
+ self.data[y, x] = color
+ self.update_image()
+
+ def get_row(self, index: int) -> Generator[int, None, None]:
"""Return a row from self. Does not delete data from internal grid."""
for x in range(self.size[0]):
- tile = self.get_info((x, index))
- assert tile is not None
- yield tile
+ yield self.get_info((x, index))
- def get_column(self, index: int) -> Generator[Tile, None, None]:
+ def get_column(self, index: int) -> Generator[int, None, None]:
"""Return a column from self. Does not delete data from internal grid."""
for y in range(self.size[1]):
- tile = self.get_info((index, y))
- assert tile is not None
- yield tile
+ yield self.get_info((index, y))
def get_colors_in_row(
self,
index: int,
exclude_negatives: bool = True,
- ) -> list[int]:
+ ) -> set[int]:
"""Return the colors placed in a given row in internal grid."""
- row_colors = [tile.color for tile in self.get_row(index)]
+ row_colors: Iterable[int] = self.get_row(index)
if exclude_negatives:
- row_colors = [c for c in row_colors if c >= 0]
- ccolors = Counter(row_colors)
- return sorted(ccolors.keys())
+ row_colors = (c for c in row_colors if c >= 0)
+ return set(row_colors)
def get_colors_in_column(
self,
index: int,
exclude_negatives: bool = True,
- ) -> list[int]:
+ ) -> set[int]:
"""Return the colors placed in a given row in internal grid."""
- column_colors = [tile.color for tile in self.get_column(index)]
+ column_colors: Iterable[int] = self.get_column(index)
if exclude_negatives:
- column_colors = [c for c in column_colors if c >= 0]
- ccolors = Counter(column_colors)
- return sorted(ccolors.keys())
+ column_colors = (c for c in column_colors if c >= 0)
+ return set(column_colors)
def is_wall_tiling(self) -> bool:
"""Return True if in Wall Tiling Mode."""
return self.wall_tiling
- def get_tile_for_cursor_by_row(self, row: int) -> Tile | None:
- """Return A COPY OF tile the mouse should hold. Returns None on failure."""
- if row in self.additions:
- data = self.additions[row]
- if isinstance(data, Tile):
- return data
- return None
-
- @gsc_bound_index(False)
def can_place_tile_color_at_point(
self,
position: tuple[int, int],
- tile: Tile,
+ tile_color: int,
) -> bool:
"""Return True if tile's color is valid at given position."""
column, row = position
- colors = set(
- self.get_colors_in_column(column) + self.get_colors_in_row(row),
+ colors = self.get_colors_in_column(column) | self.get_colors_in_row(
+ row,
)
- return tile.color not in colors
-
- def get_rows_to_tile_map(self) -> dict[int, int]:
- """Return a dictionary of row numbers and row color to be wall tiled."""
- rows = {}
- for row, tile in self.additions.items():
- if not isinstance(tile, Tile):
- continue
- rows[row] = tile.color
- return rows
-
- def calculate_valid_locations_for_tile_row(
- self,
- row: int,
- ) -> tuple[int, ...]:
- """Return the valid drop columns of the additions tile for a given row."""
- valid = []
- # ??? Why overwriting row?
- if row in self.additions:
- tile = self.additions[row]
- if isinstance(tile, Tile):
- for column in range(self.size[0]):
- if self.can_place_tile_color_at_point((column, row), tile):
- valid.append(column)
- return tuple(valid)
- return ()
-
- def remove_invalid_additions(self) -> None:
- """Remove invalid additions that would not be placeable."""
- # In the wall-tiling phase, it may happen that you
- # are not able to move the rightmost tile of a certain
- # pattern line over to the wall because there is no valid
- # space left for it. In this case, you must immediately
- # place all tiles of that pattern line in your floor line.
- for row in range(self.size[1]):
- row_tile = self.additions[row]
- if not isinstance(row_tile, Tile):
- continue
- valid = self.calculate_valid_locations_for_tile_row(row)
- if not valid:
- floor = self.player.get_object_by_name("floor_line")
- assert isinstance(floor, FloorLine)
- floor.place_tile(row_tile)
- self.additions[row] = None
-
- @gsc_bound_index(False)
- def wall_tile_from_point(self, position: tuple[int, int]) -> bool:
- """Given a position, wall tile. Return success on placement. Also updates if in wall tiling mode."""
- success = False
- column, row = position
- at_point = self.get_info(position)
- assert at_point is not None
- if at_point.color <= 0 and row in self.additions:
- tile = self.additions[row]
- if isinstance(tile, Tile) and self.can_place_tile_color_at_point(
- position,
- tile,
- ):
- self.place_tile(position, tile)
- self.additions[row] = column
- # Update invalid placements after new placement
- self.remove_invalid_additions()
- success = True
- if not self.get_rows_to_tile_map():
- self.wall_tiling = False
- return success
-
- def wall_tiling_mode(self, moved_table: dict[int, Tile]) -> None:
- """Set self into Wall Tiling Mode. Finishes automatically if not in variant play mode."""
- self.wall_tiling = True
- for key, value in moved_table.items():
- key = int(key) - 1
- if key in self.additions:
- raise RuntimeError(
- f"Key {key!r} Already in additions dictionary!",
- )
- self.additions[key] = value
- if not self.variant_play:
- for row in range(self.size[1]):
- if row in self.additions:
- rowdata = [tile.color for tile in self.get_row(row)]
- tile = self.additions[row]
- if not isinstance(tile, Tile):
- continue
- negative_tile_color = -(tile.color + 1)
- if negative_tile_color in rowdata:
- column = rowdata.index(negative_tile_color)
- self.place_tile((column, row), tile)
- # Set data to the column placed in, use for scoring
- self.additions[row] = column
- else:
- raise RuntimeError(
- f"{negative_tile_color} not in row {row}!",
- )
- else:
- raise RuntimeError(f"{row} not in moved_table!")
- self.wall_tiling = False
- else:
- # Invalid additions can only happen in variant play mode.
- self.remove_invalid_additions()
+ return tile_color not in colors
+
+ ## def remove_invalid_additions(self) -> None:
+ ## """Remove invalid additions that would not be placeable."""
+ ## # In the wall-tiling phase, it may happen that you
+ ## # are not able to move the rightmost tile of a certain
+ ## # pattern line over to the wall because there is no valid
+ ## # space left for it. In this case, you must immediately
+ ## # place all tiles of that pattern line in your floor line.
+ ## for row in range(self.size[1]):
+ ## row_tile = self.additions[row]
+ ## if not isinstance(row_tile, int):
+ ## continue
+ ## valid = self.calculate_valid_locations_for_tile_row(row)
+ ## if not valid:
+ ## floor = self.player.get_object_by_name("floor_line")
+ ## assert isinstance(floor, FloorLine)
+ ## floor.place_tile(row_tile)
+ ## self.additions[row] = None
+
+ ## def wall_tile_from_point(self, position: tuple[int, int]) -> bool:
+ ## """Given a position, wall tile. Return success on placement. Also updates if in wall tiling mode."""
+ ## success = False
+ ## column, row = position
+ ## at_point = self.get_info(position)
+ ## assert at_point is not None
+ ## if at_point.color <= 0 and row in self.additions:
+ ## tile = self.additions[row]
+ ## if isinstance(tile, int) and self.can_place_tile_color_at_point(
+ ## position,
+ ## tile,
+ ## ):
+ ## self.place_tile(position, tile)
+ ## self.additions[row] = column
+ ## # Update invalid placements after new placement
+ ## self.remove_invalid_additions()
+ ## success = True
+ ## if not self.get_rows_to_tile_map():
+ ## self.wall_tiling = False
+ ## return success
+
+ ## def wall_tiling_mode(self, moved_table: dict[int, int]) -> None:
+ ## """Set self into Wall Tiling Mode. Finishes automatically if not in variant play mode."""
+ ## self.wall_tiling = True
+ ## for key, value in moved_table.items():
+ ## key = int(key) - 1
+ ## if key in self.additions:
+ ## raise RuntimeError(
+ ## f"Key {key!r} Already in additions dictionary!",
+ ## )
+ ## self.additions[key] = value
+ ## if not self.variant_play:
+ ## for row in range(self.size[1]):
+ ## if row in self.additions:
+ ## rowdata = [tile.color for tile in self.get_row(row)]
+ ## tile = self.additions[row]
+ ## if not isinstance(tile, int):
+ ## continue
+ ## negative_tile_color = -(tile.color + 1)
+ ## if negative_tile_color in rowdata:
+ ## column = rowdata.index(negative_tile_color)
+ ## self.place_tile((column, row), tile)
+ ## # Set data to the column placed in, use for scoring
+ ## self.additions[row] = column
+ ## else:
+ ## raise RuntimeError(
+ ## f"{negative_tile_color} not in row {row}!",
+ ## )
+ ## else:
+ ## raise RuntimeError(f"{row} not in moved_table!")
+ ## self.wall_tiling = False
+ ## else:
+ ## # Invalid additions can only happen in variant play mode.
+ ## self.remove_invalid_additions()
- @gsc_bound_index(([], []))
def get_touches_continuous(
self,
xy: tuple[int, int],
- ) -> tuple[list[Tile], list[Tile]]:
+ ) -> tuple[list[int], list[int]]:
"""Return two lists, each of which contain all the tiles that touch the tile at given x y position, including that position."""
rs, cs = self.size
x, y = xy
# Get row and column tile color data
- row = [tile.color for tile in self.get_row(y)]
- column = [tile.color for tile in self.get_column(x)]
+ row = list(self.get_row(y))
+ column = list(self.get_column(x))
# Both
def get_greater_than(v: int, size: int, data: list[int]) -> list[int]:
@@ -1187,12 +1107,10 @@ def comb(one: Iterable[T], two: Iterable[RT]) -> list[tuple[T, RT]]:
"""Combine two lists by zipping together and returning list object."""
return list(zip(one, two, strict=False))
- def get_all(lst: list[tuple[int, int]]) -> Generator[Tile, None, None]:
+ def get_all(lst: list[tuple[int, int]]) -> Generator[int, None, None]:
"""Return all of the self.get_info points for each value in lst."""
for pos in lst:
- tile = self.get_info(pos)
- assert tile is not None
- yield tile
+ yield self.get_info(pos)
# Get row touches
row_touches = comb(get_greater_than(x, rs, row), [y] * rs)
@@ -1226,7 +1144,7 @@ def get_filled_rows(self) -> int:
"""Return the number of filled rows on this board."""
count = 0
for row in range(self.size[1]):
- real = (t.color >= 0 for t in self.get_row(row))
+ real = (t >= 0 for t in self.get_row(row))
if all(real):
count += 1
return count
@@ -1239,19 +1157,18 @@ def get_filled_columns(self) -> int:
"""Return the number of filled rows on this board."""
count = 0
for column in range(self.size[0]):
- real = (t.color >= 0 for t in self.get_column(column))
+ real = (t >= 0 for t in self.get_column(column))
if all(real):
count += 1
return count
def get_filled_colors(self) -> int:
"""Return the number of completed colors on this board."""
- tiles = (
+ color_count = Counter(
self.get_info((x, y))
for x in range(self.size[0])
for y in range(self.size[1])
)
- color_count = Counter(t.color for t in tiles if t is not None)
count = 0
for fill_count in color_count.values():
if fill_count >= 5:
@@ -1266,12 +1183,6 @@ def end_of_game_scoreing(self) -> int:
score += self.get_filled_colors() * 10
return score
- def process(self, time_passed: float) -> None:
- """Process board."""
- if self.image_update and not self.variant_play:
- self.set_colors(True)
- super().process(time_passed)
-
class Row(TileRenderer):
"""Represents one of the five rows each player has."""
@@ -1281,30 +1192,24 @@ class Row(TileRenderer):
def __init__(
self,
- player: Player,
size: int,
- tile_seperation: int | None = None,
+ tile_separation: int | None = None,
background: tuple[int, int, int] | None = None,
) -> None:
"""Initialize row."""
super().__init__(
"Row",
- player.game,
- tile_seperation,
+ tile_separation,
background,
)
- self.player = player
self.size = int(size)
- self.color = -6
- self.tiles = deque([Tile(self.color)] * self.size)
+ self.color = Tile.blank
+ self.tiles = list([self.color] * self.size)
def __repr__(self) -> str:
"""Return representation of self."""
- return f"{self.__class__.__name__}(%r, %i, ...)" % (
- self.game,
- self.size,
- )
+ return f"{self.__class__.__name__}({self.size})"
def update_image(self) -> None:
"""Update self.image."""
@@ -1315,16 +1220,10 @@ def update_image(self) -> None:
def get_tile_point(self, screen_location: tuple[int, int]) -> int | None:
"""Return the xy choordinates of which tile intersects given a point. Returns None if no intersections."""
- # `Grid.get_tile_point` inlined
- # Can't get tile if screen location doesn't intersect our hitbox!
- if not self.is_selected(screen_location):
+ pos = super().get_tile_point()
+ if pos is None:
return None
- # Otherwise, find out where screen point is in image locations
- # board x and y
- bx, _by = self.to_image_surface_location(screen_location)
- # Finally, return the full divides (no decimals) of xy location by self.tile_full.
-
- return self.size - 1 - int(bx // self.tile_full)
+ return pos[0]
def get_placed(self) -> int:
"""Return the number of tiles in self that are not fake tiles, like grey ones."""
@@ -1338,14 +1237,14 @@ def is_full(self) -> bool:
"""Return True if this row is full."""
return self.get_placed() == self.size
- def get_info(self, location: int) -> Tile | None:
+ def get_info(self, location: int) -> int | None:
"""Return tile at location without deleting it. Return None on invalid location."""
index = self.size - 1 - location
if index < 0 or index > len(self.tiles):
return None
return self.tiles[index]
- def can_place(self, tile: Tile) -> bool:
+ def can_place(self, tile: int) -> bool:
"""Return True if permitted to place given tile object on self."""
placeable = (tile.color == self.color) or (
self.color < 0 and tile.color >= 0
@@ -1366,14 +1265,14 @@ def can_place(self, tile: Tile) -> bool:
self.size - 1,
)
- def get_tile(self, replace: int = -6) -> Tile:
+ def get_tile(self, replace: int = Tile.blank) -> int:
"""Return the leftmost tile while deleting it from self."""
- self.tiles.appendleft(Tile(replace))
+ self.tiles.appendleft(int(replace))
self.image_update = True
return self.tiles.pop()
- def place_tile(self, tile: Tile) -> None:
- """Place a given Tile Object on self if permitted."""
+ def place_tile(self, tile: int) -> None:
+ """Place a given int Object on self if permitted."""
if self.can_place(tile):
self.color = tile.color
self.tiles.append(tile)
@@ -1386,7 +1285,7 @@ def place_tile(self, tile: Tile) -> None:
else:
raise ValueError("Not allowed to place.")
- def can_place_tiles(self, tiles: list[Tile]) -> bool:
+ def can_place_tiles(self, tiles: list[int]) -> bool:
"""Return True if permitted to place all of given tiles objects on self."""
if len(tiles) > self.get_placeable():
return False
@@ -1399,7 +1298,7 @@ def can_place_tiles(self, tiles: list[Tile]) -> bool:
tile_colors.append(tile.color)
return not len(tile_colors) > 1
- def place_tiles(self, tiles: list[Tile]) -> None:
+ def place_tiles(self, tiles: list[int]) -> None:
"""Place multiple tile objects on self if permitted."""
if self.can_place_tiles(tiles):
for tile in tiles:
@@ -1407,41 +1306,41 @@ def place_tiles(self, tiles: list[Tile]) -> None:
else:
raise ValueError("Not allowed to place tiles.")
- def wall_tile(
- self,
- add_to_table: dict[str, list[Tile] | Tile | None],
- empty_color: int = -6,
- ) -> None:
- """Move tiles around and into add dictionary for the wall tiling phase of the game. Removes tiles from self."""
- if "tiles_for_box" not in add_to_table:
- add_to_table["tiles_for_box"] = []
- if not self.is_full():
- add_to_table[str(self.size)] = None
- return
- self.color = empty_color
- add_to_table[str(self.size)] = self.get_tile()
- for_box = add_to_table["tiles_for_box"]
- assert isinstance(for_box, list)
- for _i in range(self.size - 1):
- for_box.append(self.get_tile())
+ ## def wall_tile(
+ ## self,
+ ## add_to_table: dict[str, list[int] | int | None],
+ ## empty_color: int = Tile.blank,
+ ## ) -> None:
+ ## """Move tiles around and into add dictionary for the wall tiling phase of the game. Removes tiles from self."""
+ ## if "tiles_for_box" not in add_to_table:
+ ## add_to_table["tiles_for_box"] = []
+ ## if not self.is_full():
+ ## add_to_table[str(self.size)] = None
+ ## return
+ ## self.color = empty_color
+ ## add_to_table[str(self.size)] = self.get_tile()
+ ## for_box = add_to_table["tiles_for_box"]
+ ## assert isinstance(for_box, list)
+ ## for _i in range(self.size - 1):
+ ## for_box.append(self.get_tile())
def set_background(self, color: tuple[int, int, int] | None) -> None:
"""Set the background color for this row."""
self.background = color
- self.image_update = True
+ self.update_image()
class PatternLine(MultipartObject):
"""Represents multiple rows to make the pattern line."""
- __slots__ = ("player", "row_seperation")
+ __slots__ = ("player", "row_separation")
size = (5, 5)
- def __init__(self, player: Player, row_seperation: int = 0) -> None:
+ def __init__(self, player: Player, row_separation: int = 0) -> None:
"""Initialize pattern line."""
super().__init__("PatternLine")
self.player = player
- self.row_seperation = row_seperation
+ self.row_separation = row_separation
for x, _y in zip(
range(self.size[0]),
@@ -1468,14 +1367,14 @@ def get_row(self, row: int) -> Row:
def reset_position(self) -> None:
"""Reset Locations of Rows according to self.location."""
last = self.size[1]
- w = self.get_row(last - 1).wh[0]
+ w = self.get_row(last - 1).width_height[0]
if w is None:
raise RuntimeError(
- "Image Dimensions for Row Object (row.wh) are None!",
+ "Image Dimensions for Row Object (row.width_height) are None!",
)
h1 = self.get_row(0).tile_full
h = int(last * h1)
- self.wh = w, h
+ self.width_height = w, h
w1 = h1 / 2
x, y = self.location
@@ -1500,9 +1399,9 @@ def is_full(self) -> bool:
"""Return True if self is full."""
return all(self.get_row(rid).is_full() for rid in range(self.size[1]))
- def wall_tiling(self) -> dict[str, list[Tile] | Tile | None]:
+ def wall_tiling(self) -> dict[str, list[int] | int | None]:
"""Return a dictionary to be used with wall tiling. Removes tiles from rows."""
- values: dict[str, list[Tile] | Tile | None] = {}
+ values: dict[str, list[int] | int | None] = {}
for rid in range(self.size[1]):
self.get_row(rid).wall_tile(values)
return values
@@ -1518,7 +1417,7 @@ class FloorLine(Row):
"""Represents a player's floor line."""
size = 7
- number_one_color = Tiles.one
+ number_one_color = Tile.one
def __init__(self, player: Player) -> None:
"""Initialize floor line."""
@@ -1546,13 +1445,13 @@ def render(self, surface: pygame.surface.Surface) -> None:
super().render(surface)
sx, sy = self.location
- assert self.wh is not None, "Should be impossible."
- w, h = self.wh
+ assert self.width_height is not None, "Should be impossible."
+ w, h = self.width_height
for x in range(self.size):
xy = round(
- x * self.tile_full + self.tile_seperation + sx - w / 2,
+ x * self.tile_full + self.tile_separation + sx - w / 2,
), round(
- self.tile_seperation + sy - h / 2,
+ self.tile_separation + sy - h / 2,
)
self.text.update_value(str(self.numbers[x]))
self.text.location = Vector2(*xy)
@@ -1560,8 +1459,8 @@ def render(self, surface: pygame.surface.Surface) -> None:
# self.font.render(surface, str(self.numbers[x]), xy)
- def place_tile(self, tile: Tile) -> None:
- """Place a given Tile Object on self if permitted."""
+ def place_tile(self, tile: int) -> None:
+ """Place a given int Object on self if permitted."""
self.tiles.insert(self.get_placed(), tile)
if tile.color == self.number_one_color:
@@ -1570,7 +1469,7 @@ def place_tile(self, tile: Tile) -> None:
box_lid = self.player.game.get_object_by_name("BoxLid")
assert isinstance(box_lid, BoxLid)
- def handle_end(end: Tile) -> None:
+ def handle_end(end: int) -> None:
"""Handle the end tile we are replacing. Ensures number one tile is not removed."""
if not end.color < 0:
if end.color == self.number_one_color:
@@ -1597,8 +1496,8 @@ def score_tiles(self) -> int:
def get_tiles(
self,
- empty_color: int = -6,
- ) -> tuple[list[Tile], Tile | None]:
+ empty_color: int = Tile.blank,
+ ) -> tuple[list[int], int | None]:
"""Return tuple of tiles gathered, and then either the number one tile or None."""
tiles = []
number_one_tile = None
@@ -1610,11 +1509,11 @@ def get_tiles(
tiles.append(tile)
for _i in range(self.size):
- self.tiles.append(Tile(empty_color))
+ self.tiles.append(int(empty_color))
self.image_update = True
return tiles, number_one_tile
- def can_place_tiles(self, tiles: list[Tile]) -> bool:
+ def can_place_tiles(self, tiles: list[int]) -> bool:
"""Return True."""
return True
@@ -1627,9 +1526,9 @@ class Factory(Grid):
outline = BLUE
out_size = 0.1
- def __init__(self, game: Game, factory_id: int) -> None:
+ def __init__(self, factory_id: int) -> None:
"""Initialize factory."""
- super().__init__(self.size, game, background=None)
+ super().__init__(self.size, background=None)
self.number = factory_id
self.name = f"Factory{self.number}"
@@ -1639,14 +1538,12 @@ def __init__(self, game: Game, factory_id: int) -> None:
def __repr__(self) -> str:
"""Return representation of self."""
- return f"{self.__class__.__name__}(%r, %i)" % (self.game, self.number)
+ return f"{self.__class__.__name__}({self.number})"
def add_circle(self, surface: pygame.surface.Surface) -> None:
"""Add circle to self.image."""
- # if f"FactoryCircle{self.radius}" not in self.game.cache:
rad = math.ceil(self.radius)
surf = pygame.surface.Surface((2 * rad, 2 * rad), SRCALPHA)
-## surf = set_alpha(, 1)
pygame.draw.circle(surf, self.outline, (rad, rad), rad)
pygame.draw.circle(
surf,
@@ -1654,8 +1551,7 @@ def add_circle(self, surface: pygame.surface.Surface) -> None:
(rad, rad),
math.ceil(rad * (1 - self.out_size)),
)
- # self.game.cache[f"FactoryCircle{self.radius}"] = surf
- # surf = self.game.cache[f"FactoryCircle{self.radius}"].copy()
+
surface.blit(
surf,
(
@@ -1670,7 +1566,7 @@ def render(self, surface: pygame.surface.Surface) -> None:
self.add_circle(surface)
super().render(surface)
- def fill(self, tiles: list[Tile]) -> None:
+ def fill(self, tiles: list[int]) -> None:
"""Fill self with tiles. Will raise exception if insufficiant tiles."""
if len(tiles) < self.size[0] * self.size[1]:
size = self.size[0] * self.size[1]
@@ -1687,19 +1583,19 @@ def fill(self, tiles: list[Tile]) -> None:
if tiles:
raise RuntimeError("Too many tiles!")
- def grab(self) -> list[Tile]:
+ def grab(self) -> list[int]:
"""Return all tiles on this factory."""
return [
tile
for tile in (
- self.get_tile((x, y), -6)
+ self.get_tile((x, y), Tile.blank)
for x in range(self.size[0])
for y in range(self.size[1])
)
- if tile is not None and tile.color != -6
+ if tile is not None and tile.color != Tile.blank
]
- def grab_color(self, color: int) -> tuple[list[Tile], list[Tile]]:
+ def grab_color(self, color: int) -> tuple[list[int], list[int]]:
"""Return all tiles of color given in the first list, and all non-matches in the second list."""
tiles = self.grab()
right, wrong = [], []
@@ -1722,7 +1618,7 @@ def process(self, time_passed: float) -> None:
class Factories(MultipartObject):
"""Factories Multipart Object, made of multiple Factory Objects."""
- teach = 4
+ tiles_each = 4
def __init__(
self,
@@ -1760,12 +1656,13 @@ def __repr__(self) -> str:
def reset_position(self) -> None:
"""Reset the position of all factories within."""
- degrees = 360 / self.count
- for i in range(self.count):
- radians = math.radians(degrees * i)
- self.objects[i].location = Vector2(
- math.sin(radians) * self.size + self.location[0],
- math.cos(radians) * self.size + self.location[1],
+ for index, degrees in enumerate(range(0, 360, 360 // self.count)):
+ self.objects[index].location = (
+ Vector2.from_degrees(
+ degrees,
+ self.size,
+ )
+ + self.location
)
def process(self, time_passed: float) -> None:
@@ -1797,13 +1694,13 @@ def process(self, time_passed: float) -> None:
table.add_tiles(tocenter)
cursor.drag(select)
- def play_tiles_from_bag(self, empty_color: int = -6) -> None:
+ def play_tiles_from_bag(self, empty_color: int = Tile.blank) -> None:
"""Divy up tiles to each factory from the bag."""
# For every factory we have,
for fid in range(self.count):
# Draw tiles for the factory
drawn = []
- for _i in range(self.teach):
+ for _i in range(self.tiles_each):
# If the bag is not empty,
if not self.game.bag.is_empty():
# Draw a tile from the bag.
@@ -1828,7 +1725,7 @@ def play_tiles_from_bag(self, empty_color: int = -6) -> None:
# while there are none left in the lid, start a new
# round as usual even though are not all factory
# displays are properly filled."
- drawn.append(Tile(empty_color))
+ drawn.append(int(empty_color))
# Place drawn tiles on factory
factory = self.objects[fid]
assert isinstance(factory, Factory)
@@ -1844,245 +1741,98 @@ def is_all_empty(self) -> bool:
return True
-class TableCenter(Grid):
+class TableCenter(TileRenderer):
"""Object that represents the center of the table."""
+ __slots__ = ("tiles",)
size = (6, 6)
- first_tile_color = Tiles.one
- def __init__(self, game: Game, has_number_one_tile: bool = True) -> None:
+ def __init__(self) -> None:
"""Initialize center of table."""
- super().__init__(self.size, game, background=None)
- self.game = game
- self.name = "TableCenter"
-
- self.number_one_tile_exists = False
- if has_number_one_tile:
- self.add_number_one_tile()
-
- self.next_position = (0, 0)
+ super().__init__("TableCenter", background=None)
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"{self.__class__.__name__}({self.game!r})"
-
- def add_number_one_tile(self) -> None:
- """Add the number one tile to the internal grid."""
- if not self.number_one_tile_exists:
- x, y = self.size
- self.place_tile((x - 1, y - 1), Tile(self.first_tile_color))
- self.number_one_tile_exists = True
-
- def add_tile(self, tile: Tile) -> None:
- """Add a Tile Object to the Table Center Grid."""
- self.place_tile(self.next_position, tile)
- x, y = self.next_position
- x += 1
- y += int(x // self.size[0])
- x %= self.size[0]
- y %= self.size[1]
- self.next_position = (x, y)
- self.image_update = True
-
- def add_tiles(self, tiles: Iterable[Tile], sort: bool = True) -> None:
- """Add multiple Tile Objects to the Table Center Grid."""
- for tile in tiles:
- self.add_tile(tile)
- if sort and tiles:
- self.reorder_tiles()
-
- def reorder_tiles(self, replace: int = -6) -> None:
- """Re-organize tiles by Color."""
- full = []
- for y in range(self.size[1]):
- for x in range(self.size[0]):
- if self.number_one_tile_exists:
- tile = self.get_info((x, y))
- assert tile is not None
- if tile.color == self.first_tile_color:
- continue
- at = self.get_tile((x, y), replace)
-
- if at is not None:
- full.append(at)
- sorted_tiles = sorted(full, key=sort_tiles)
- self.next_position = (0, 0)
- self.add_tiles(sorted_tiles, False)
-
- def pull_tiles(self, tile_color: int, replace: int = -6) -> list[Tile]:
- """Remove all of the tiles of tile_color from the Table Center Grid."""
- to_pull: list[tuple[int, int]] = []
- for y in range(self.size[1]):
- for x in range(self.size[0]):
- info_tile = self.get_info((x, y))
- assert info_tile is not None
- if info_tile.color == tile_color:
- to_pull.append((x, y))
- elif (
- self.number_one_tile_exists
- and info_tile.color == self.first_tile_color
- ):
- to_pull.append((x, y))
- self.number_one_tile_exists = False
- tiles = []
- for pos in to_pull:
- tile = self.get_tile(pos, replace)
- assert tile is not None
- tiles.append(tile)
- self.reorder_tiles(replace)
- return tiles
-
- def process(self, time_passed: float) -> None:
- """Process factories."""
- if self.hidden:
- super().process(time_passed)
- return
- cursor = self.game.get_object_by_name("Cursor")
- assert isinstance(cursor, Cursor)
- if (
- cursor.is_pressed()
- and not cursor.is_holding()
- and not self.is_empty()
- and self.is_selected(cursor.location)
- ):
- point = self.get_tile_point(cursor.location)
- # Shouldn't return none anymore since we have is_selected now.
- assert point is not None
- tile = self.get_info(point)
- assert isinstance(tile, Tile)
- color_at_point = tile.color
- if color_at_point >= 0 and color_at_point < 5:
- cursor.drag(self.pull_tiles(color_at_point))
- super().process(time_passed)
-
-
-class Bag(Component):
- """Represents the bag full of tiles."""
-
- __slots__ = (
- "percent_each",
- "tile_count",
- "tile_names",
- "tile_types",
- "tiles",
- )
-
- def __init__(self, tile_count: int = 100, tile_types: int = 5) -> None:
- """Initialize bag of tiles."""
- super().__init__("bag")
- self.tile_count = int(tile_count)
- self.tile_types = int(tile_types)
- self.tile_names = [chr(65 + i) for i in range(self.tile_types)]
- self.percent_each = (self.tile_count / self.tile_types) / 100
- self.tiles: deque[str]
- self.full_reset()
-
- def full_reset(self) -> None:
- """Reset the bag to a full, re-randomized bag."""
- self.tiles = deque(
- gen_random_proper_seq(
- self.tile_count,
- **dict.fromkeys(self.tile_names, self.percent_each),
- ),
- )
-
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"{self.__class__.__name__}(%i, %i)" % (
- self.tile_count,
- self.tile_types,
- )
-
- def reset(self) -> None:
- """Randomize all the tiles in the bag."""
- self.tiles = deque(randomize(self.tiles))
-
- def get_color(self, tile_name: str) -> int:
- """Return the color of a named tile."""
- if tile_name not in self.tile_names:
- raise ValueError(f"Tile Name {tile_name} Not Found!")
- return self.tile_names.index(tile_name)
-
- def get_tile(self, tile_name: str) -> Tile:
- """Return a Tile Object from a tile name."""
- return Tile(self.get_color(tile_name))
-
- def get_count(self) -> int:
- """Return number of tiles currently held."""
- return len(self.tiles)
-
- def is_empty(self) -> bool:
- """Return True if no tiles are currently held."""
- return self.get_count() == 0
-
- def draw_tile(self) -> Tile | None:
- """Return a random Tile Object from the bag. Return None if no tiles to draw."""
- if not self.is_empty():
- return self.get_tile(self.tiles.pop())
- return None
-
- def get_name(self, tile_color: int) -> str:
- """Return the name of a tile given it's color."""
- try:
- return self.tile_names[tile_color]
- except IndexError as exc:
- raise ValueError("Invalid Tile Color!") from exc
-
- def add_tile(self, tile_object: Tile) -> None:
- """Add a Tile Object to the bag."""
- name = self.get_name(int(tile_object.color))
- range_ = (0, len(self.tiles) - 1)
- if range_[1] - range_[0] <= 1:
- index = 0
- else:
- # S311 Standard pseudo-random generators are not suitable for cryptographic purposes
- index = random.randint(range_[0], range_[1]) # noqa: S311
- # self.tiles.insert(random.randint(0, len(self.tiles)-1), self.get_name(int(tile_object.color)))
- self.tiles.insert(index, name)
- del tile_object
-
- def add_tiles(self, tile_objects: Iterable[Tile]) -> None:
- """Add multiple Tile Objects to the bag."""
- for tile_object in tile_objects:
- self.add_tile(tile_object)
-
-
-class BoxLid(Component):
- """BoxLid Object, represents the box lid were tiles go before being added to the bag again."""
-
- def __init__(self) -> None:
- """Initialize box lid."""
- super().__init__("BoxLid")
- self.tiles: deque[Tile] = deque()
+ self.tiles: Counter[int] = Counter()
+ self.update_image()
+ self.visible = True
def __repr__(self) -> str:
"""Return representation of self."""
return f"{self.__class__.__name__}()"
- def add_tile(self, tile: Tile) -> None:
- """Add a tile to self."""
- if tile.color >= 0 and tile.color < 5:
- self.tiles.append(tile)
- return
- raise ValueError(
- f"BoxLid.add_tile tried to add an invalid tile to self ({tile.color = }).",
- )
+ def iter_tiles(self) -> Generator[int, None, None]:
+ """Yield tile colors."""
+ count = 0
+ for tile_type in sorted(set(self.tiles) - {Tile.one}):
+ tile_count = self.tiles[tile_type]
+ for _ in range(tile_count):
+ yield tile_type
+ count += 1
- def add_tiles(self, tiles: Iterable[Tile]) -> None:
- """Add multiple tiles to self."""
- for tile in tiles:
- self.add_tile(tile)
+ width, height = self.size
+ remaining = width * height - count
- def get_tiles(self) -> list[Tile]:
- """Return all tiles in self while deleting them from self."""
- return [self.tiles.popleft() for i in range(len(self.tiles))]
+ one_count = self.tiles.get(Tile.one, 0)
+ remaining = max(remaining - one_count, 0)
+ for _ in range(remaining):
+ yield Tile.blank
+ for _ in range(one_count):
+ yield Tile.one
- def is_empty(self) -> bool:
- """Return True if self is empty (no tiles on it)."""
- return len(self.tiles) == 0
+ def update_image(self):
+ """Reset/update image."""
+ self.clear_image(self.size)
+ width, height = self.size
+ tile_generator = self.iter_tiles()
+ for y in range(height):
+ for x in range(width):
+ tile = next(tile_generator)
+ # if tile == Tile.blank:
+ # continue
+ self.blit_tile(tile, (x, y))
+ self.dirty = 1
-class Player(MultipartObject):
+ def add_tile(self, tile: int) -> None:
+ """Add a tile to the center of the table."""
+ self.tiles.update((tile,))
+ self.update_image()
+
+ def add_tiles(self, tiles: Iterable[int]) -> None:
+ """Add multiple int Objects to the Table Center Grid."""
+ self.tiles.update(tiles)
+ self.update_image()
+
+ def pull_tiles(self, tile_color: int) -> list[int]:
+ """Pop all of tile_color. Raises KeyError if not exists."""
+ tile_count = self.tiles.pop(tile_color)
+ return [tile_color] * tile_count
+
+
+## def process(self, time_passed: float) -> None:
+## """Process factories."""
+## if self.hidden:
+## super().process(time_passed)
+## return
+## cursor = self.game.get_object_by_name("Cursor")
+## assert isinstance(cursor, Cursor)
+## if (
+## cursor.is_pressed()
+## and not cursor.is_holding()
+## and not self.is_empty()
+## and self.is_selected(cursor.location)
+## ):
+## point = self.get_tile_point(cursor.location)
+## # Shouldn't return none anymore since we have is_selected now.
+## assert point is not None
+## tile = self.get_info(point)
+## assert isinstance(tile, int)
+## color_at_point = tile.color
+## if color_at_point >= 0 and color_at_point < 5:
+## cursor.drag(self.pull_tiles(color_at_point))
+## super().process(time_passed)
+
+
+class Player(sprite.Sprite):
"""Represents a player. Made of lots of objects."""
def __init__(
@@ -2100,10 +1850,10 @@ def __init__(
self.networked = networked
self.varient_play = varient_play
- self.add_object(Board(self, self.varient_play))
+ self.add_object(Board(self.varient_play))
self.add_object(PatternLine(self))
self.add_object(FloorLine(self))
- self.add_object(Text(SCOREFONTSIZE, SCORECOLOR))
+ ## self.add_object(objects.Text(SCOREFONTSIZE, SCORECOLOR))
self.score = 0
self.is_turn = False
@@ -2124,11 +1874,11 @@ def __repr__(self) -> str:
self.varient_play,
)
- def update_score(self) -> None:
- """Update the scorebox for this player."""
- score_box = self.get_object_by_name("Text")
- assert isinstance(score_box, Text)
- score_box.update_value(f"Player {self.player_id + 1}: {self.score}")
+ ## def update_score(self) -> None:
+ ## """Update the scorebox for this player."""
+ ## score_box = self.get_object_by_name("Text")
+ ## assert isinstance(score_box, Text)
+ ## score_box.update_value(f"Player {self.player_id + 1}: {self.score}")
def trigger_turn_now(self) -> None:
"""Handle start of turn."""
@@ -2157,17 +1907,17 @@ def end_of_turn(self) -> None:
pattern_line.set_background(None)
self.is_turn = False
- def end_of_game_trigger(self) -> None:
- """Handle end of game.
-
- Called by end state when game is over
- Hide pattern lines and floor line.
- """
- pattern = self.get_object_by_name("PatternLine")
- floor = self.get_object_by_name("floor_line")
-
- pattern.hidden = True
- floor.hidden = True
+ ## def end_of_game_trigger(self) -> None:
+ ## """Handle end of game.
+ ##
+ ## Called by end state when game is over
+ ## Hide pattern lines and floor line.
+ ## """
+ ## pattern = self.get_object_by_name("PatternLine")
+ ## floor = self.get_object_by_name("floor_line")
+ ##
+ ## pattern.hidden = True
+ ## floor.hidden = True
def reset_position(self) -> None:
"""Reset positions of all parts of self based off self.location."""
@@ -2175,17 +1925,16 @@ def reset_position(self) -> None:
board = self.get_object_by_name("Board")
assert isinstance(board, Board)
- bw, bh = board.wh
+ bw, bh = board.width_height
board.location = Vector2(x + bw // 2, y)
pattern_line = self.get_object_by_name("PatternLine")
assert isinstance(pattern_line, PatternLine)
- lw = pattern_line.wh[0] // 2
+ lw = pattern_line.width_height[0] // 2
pattern_line.location = Vector2(x - lw, y)
floor_line = self.get_object_by_name("floor_line")
assert isinstance(floor_line, FloorLine)
- floor_line.wh[0]
floor_line.location = Vector2(
int(x - lw * (2 / 3) + TILESIZE / 3.75),
int(y + bh * (2 / 3)),
@@ -2195,217 +1944,218 @@ def reset_position(self) -> None:
assert isinstance(text, Text)
text.location = Vector2(x - (bw // 3), y - (bh * 2 // 3))
- def wall_tiling(self) -> None:
- """Do the wall tiling phase of the game for this player."""
- self.is_wall_tiling = True
- pattern_line = self.get_object_by_name("PatternLine")
- assert isinstance(pattern_line, PatternLine)
- board = self.get_object_by_name("Board")
- assert isinstance(board, Board)
- box_lid = self.game.get_object_by_name("BoxLid")
- assert isinstance(box_lid, BoxLid)
-
- data = pattern_line.wall_tiling()
- tiles_for_box = data["tiles_for_box"]
- assert isinstance(tiles_for_box, list)
- box_lid.add_tiles(tiles_for_box)
- del data["tiles_for_box"]
-
- cleaned = {}
- for key, value in data.items():
- if not isinstance(value, Tile):
- continue
- cleaned[int(key)] = value
-
- board.wall_tiling_mode(cleaned)
-
- def done_wall_tiling(self) -> bool:
- """Return True if internal Board is done wall tiling."""
- board = self.get_object_by_name("Board")
- assert isinstance(board, Board)
- return not board.is_wall_tiling()
-
- def next_round(self) -> None:
- """Handle end of wall tiling."""
- self.is_wall_tiling = False
-
- def score_phase(self) -> Tile | None:
- """Do the scoring phase of the game for this player. Return number one tile or None."""
- board = self.get_object_by_name("Board")
- floor_line = self.get_object_by_name("floor_line")
- box_lid = self.game.get_object_by_name("BoxLid")
- assert isinstance(board, Board)
- assert isinstance(floor_line, FloorLine)
- assert isinstance(box_lid, BoxLid)
-
- def saturatescore() -> None:
- if self.score < 0:
- self.score = 0
-
- self.score += board.score_additions()
- self.score += floor_line.score_tiles()
- saturatescore()
-
- tiles_for_box, number_one = floor_line.get_tiles()
- box_lid.add_tiles(tiles_for_box)
-
- self.update_score()
-
- return number_one
- def end_of_game_scoring(self) -> None:
- """Update final score with additional end of game points."""
- board = self.get_object_by_name("Board")
- assert isinstance(board, Board)
-
- self.score += board.end_of_game_scoreing()
-
- self.update_score()
-
- def has_horzontal_line(self) -> bool:
- """Return True if this player has a horizontal line on their game board filled."""
- board = self.get_object_by_name("Board")
- assert isinstance(board, Board)
-
- return board.has_filled_row()
-
- def get_horizontal_lines(self) -> int:
- """Return the number of filled horizontal lines this player has on their game board."""
- board = self.get_object_by_name("Board")
- assert isinstance(board, Board)
-
- return board.get_filled_rows()
-
- def process(self, time_passed: float) -> None:
- """Process Player."""
- if not self.is_turn: # Is our turn?
- self.set_attr_all("hidden", self.hidden)
- super().process(time_passed)
- return
- if self.hidden and self.is_wall_tiling and self.varient_play:
- # If hidden, not anymore. Our turn.
- self.hidden = False
- if self.networked: # We are networked.
- self.set_attr_all("hidden", self.hidden)
- super().process(time_passed)
- return
+## def wall_tiling(self) -> None:
+## """Do the wall tiling phase of the game for this player."""
+## self.is_wall_tiling = True
+## pattern_line = self.get_object_by_name("PatternLine")
+## assert isinstance(pattern_line, PatternLine)
+## board = self.get_object_by_name("Board")
+## assert isinstance(board, Board)
+## box_lid = self.game.get_object_by_name("BoxLid")
+## assert isinstance(box_lid, BoxLid)
+##
+## data = pattern_line.wall_tiling()
+## tiles_for_box = data["tiles_for_box"]
+## assert isinstance(tiles_for_box, list)
+## box_lid.add_tiles(tiles_for_box)
+## del data["tiles_for_box"]
+##
+## cleaned = {}
+## for key, value in data.items():
+## if not isinstance(value, int):
+## continue
+## cleaned[int(key)] = value
+##
+## board.wall_tiling_mode(cleaned)
+
+## def done_wall_tiling(self) -> bool:
+## """Return True if internal Board is done wall tiling."""
+## board = self.get_object_by_name("Board")
+## assert isinstance(board, Board)
+## return not board.is_wall_tiling()
+
+## def next_round(self) -> None:
+## """Handle end of wall tiling."""
+## self.is_wall_tiling = False
+
+## def score_phase(self) -> int | None:
+## """Do the scoring phase of the game for this player. Return number one tile or None."""
+## board = self.get_object_by_name("Board")
+## floor_line = self.get_object_by_name("floor_line")
+## box_lid = self.game.get_object_by_name("BoxLid")
+## assert isinstance(board, Board)
+## assert isinstance(floor_line, FloorLine)
+## assert isinstance(box_lid, BoxLid)
+##
+## def saturatescore() -> None:
+## if self.score < 0:
+## self.score = 0
+##
+## self.score += board.score_additions()
+## self.score += floor_line.score_tiles()
+## saturatescore()
+##
+## tiles_for_box, number_one = floor_line.get_tiles()
+## box_lid.add_tiles(tiles_for_box)
+##
+## self.update_score()
+##
+## return number_one
- cursor = self.game.get_object_by_name("Cursor")
- assert isinstance(cursor, Cursor)
- box_lid = self.game.get_object_by_name("BoxLid")
- assert isinstance(box_lid, BoxLid)
- pattern_line = self.get_object_by_name("PatternLine")
- assert isinstance(pattern_line, PatternLine)
- floor_line = self.get_object_by_name("floor_line")
- assert isinstance(floor_line, FloorLine)
- board = self.get_object_by_name("Board")
- assert isinstance(board, Board)
+## def end_of_game_scoring(self) -> None:
+## """Update final score with additional end of game points."""
+## board = self.get_object_by_name("Board")
+## assert isinstance(board, Board)
+##
+## self.score += board.end_of_game_scoreing()
+##
+## self.update_score()
- if not cursor.is_pressed():
- # Mouse up
- if self.just_held:
- self.just_held = False
- if self.just_dropped:
- self.just_dropped = False
- self.set_attr_all("hidden", self.hidden)
- super().process(time_passed)
- return
+## def has_horzontal_line(self) -> bool:
+## """Return True if this player has a horizontal line on their game board filled."""
+## board = self.get_object_by_name("Board")
+## assert isinstance(board, Board)
+##
+## return board.has_filled_row()
- # Mouse down
- obj, point = self.get_intersection(cursor.location)
- if obj is None or point is None:
- if self.is_wall_tiling and self.done_wall_tiling():
- self.next_round()
- self.game.next_turn()
- self.set_attr_all("hidden", self.hidden)
- super().process(time_passed)
- return
- # Something pressed
- if cursor.is_holding(): # Cursor holding tiles
- move_made = False
- if not self.is_wall_tiling: # Is wall tiling:
- if obj == "PatternLine":
- pos, row_number = point
- row = pattern_line.get_row(row_number)
- if not row.is_full():
- info = row.get_info(pos)
- if info is not None and info.color < 0:
- _color, _held = cursor.get_held_info()
- todrop = min(
- pos + 1,
- row.get_placeable(),
- )
- tiles = cursor.drop(todrop)
- if row.can_place_tiles(tiles):
- row.place_tiles(tiles)
- move_made = True
- else:
- cursor.force_hold(tiles)
- elif obj == "floor_line":
- tiles_to_add = cursor.drop()
- if floor_line.is_full():
- # Floor is full,
- # Add tiles to box instead.
- box_lid.add_tiles(tiles_to_add)
- elif floor_line.get_placeable() < len(
- tiles_to_add,
- ):
- # Floor is not full but cannot fit all in floor line.
- # Add tiles to floor line and then to box
- while len(tiles_to_add) > 0:
- if floor_line.get_placeable() > 0:
- floor_line.place_tile(
- tiles_to_add.pop(),
- )
- else:
- box_lid.add_tile(
- tiles_to_add.pop(),
- )
- else:
- # Otherwise add to floor line for all.
- floor_line.place_tiles(tiles_to_add)
- move_made = True
- elif not self.just_held and obj == "Board":
- tile = board.get_info(point)
- assert isinstance(tile, Tile)
- if tile.color == -6:
- # Cursor holding and wall tiling
- _column, row_id = point
- cursor_tile = cursor.drop(1)[0]
- board_tile = board.get_tile_for_cursor_by_row(
- row_id,
- )
- if (
- board_tile is not None
- and cursor_tile.color == board_tile.color
- and board.wall_tile_from_point(point)
- ):
- self.just_dropped = True
- pattern_line.get_row(
- row_id,
- ).set_background(None)
- if move_made and not self.is_wall_tiling:
- if cursor.holding_number_one:
- one_tile = cursor.drop_one_tile()
- assert one_tile is not None
- floor_line.place_tile(one_tile)
- if cursor.get_held_count(True) == 0:
- self.game.next_turn()
- elif self.is_wall_tiling and obj == "Board" and not self.just_dropped:
- # Mouse down, something pressed, and not holding anything
- # Wall tiling, pressed, not holding
- _column_number, row_number = point
- tile = board.get_tile_for_cursor_by_row(
- row_number,
- )
- if tile is not None:
- cursor.drag([tile])
- self.just_held = True
- if self.is_wall_tiling and self.done_wall_tiling():
- self.next_round()
- self.game.next_turn()
- self.set_attr_all("hidden", self.hidden)
- super().process(time_passed)
+## def get_horizontal_lines(self) -> int:
+## """Return the number of filled horizontal lines this player has on their game board."""
+## board = self.get_object_by_name("Board")
+## assert isinstance(board, Board)
+##
+## return board.get_filled_rows()
+
+## def process(self, time_passed: float) -> None:
+## """Process Player."""
+## if not self.is_turn: # Is our turn?
+## self.set_attr_all("hidden", self.hidden)
+## super().process(time_passed)
+## return
+## if self.hidden and self.is_wall_tiling and self.varient_play:
+## # If hidden, not anymore. Our turn.
+## self.hidden = False
+## if self.networked: # We are networked.
+## self.set_attr_all("hidden", self.hidden)
+## super().process(time_passed)
+## return
+##
+## cursor = self.game.get_object_by_name("Cursor")
+## assert isinstance(cursor, Cursor)
+## box_lid = self.game.get_object_by_name("BoxLid")
+## assert isinstance(box_lid, BoxLid)
+## pattern_line = self.get_object_by_name("PatternLine")
+## assert isinstance(pattern_line, PatternLine)
+## floor_line = self.get_object_by_name("floor_line")
+## assert isinstance(floor_line, FloorLine)
+## board = self.get_object_by_name("Board")
+## assert isinstance(board, Board)
+##
+## if not cursor.is_pressed():
+## # Mouse up
+## if self.just_held:
+## self.just_held = False
+## if self.just_dropped:
+## self.just_dropped = False
+## self.set_attr_all("hidden", self.hidden)
+## super().process(time_passed)
+## return
+##
+## # Mouse down
+## obj, point = self.get_intersection(cursor.location)
+## if obj is None or point is None:
+## if self.is_wall_tiling and self.done_wall_tiling():
+## self.next_round()
+## self.game.next_turn()
+## self.set_attr_all("hidden", self.hidden)
+## super().process(time_passed)
+## return
+## # Something pressed
+## if cursor.is_holding(): # Cursor holding tiles
+## move_made = False
+## if not self.is_wall_tiling: # Is wall tiling:
+## if obj == "PatternLine":
+## pos, row_number = point
+## row = pattern_line.get_row(row_number)
+## if not row.is_full():
+## info = row.get_info(pos)
+## if info is not None and info.color < 0:
+## _color, _held = cursor.get_held_info()
+## todrop = min(
+## pos + 1,
+## row.get_placeable(),
+## )
+## tiles = cursor.drop(todrop)
+## if row.can_place_tiles(tiles):
+## row.place_tiles(tiles)
+## move_made = True
+## else:
+## cursor.force_hold(tiles)
+## elif obj == "floor_line":
+## tiles_to_add = cursor.drop()
+## if floor_line.is_full():
+## # Floor is full,
+## # Add tiles to box instead.
+## box_lid.add_tiles(tiles_to_add)
+## elif floor_line.get_placeable() < len(
+## tiles_to_add,
+## ):
+## # Floor is not full but cannot fit all in floor line.
+## # Add tiles to floor line and then to box
+## while len(tiles_to_add) > 0:
+## if floor_line.get_placeable() > 0:
+## floor_line.place_tile(
+## tiles_to_add.pop(),
+## )
+## else:
+## box_lid.add_tile(
+## tiles_to_add.pop(),
+## )
+## else:
+## # Otherwise add to floor line for all.
+## floor_line.place_tiles(tiles_to_add)
+## move_made = True
+## elif not self.just_held and obj == "Board":
+## tile = board.get_info(point)
+## assert isinstance(tile, int)
+## if tile.color == Tile.blank:
+## # Cursor holding and wall tiling
+## _column, row_id = point
+## cursor_tile = cursor.drop(1)[0]
+## board_tile = board.get_tile_for_cursor_by_row(
+## row_id,
+## )
+## if (
+## board_tile is not None
+## and cursor_tile.color == board_tile.color
+## and board.wall_tile_from_point(point)
+## ):
+## self.just_dropped = True
+## pattern_line.get_row(
+## row_id,
+## ).set_background(None)
+## if move_made and not self.is_wall_tiling:
+## if cursor.holding_number_one:
+## one_tile = cursor.drop_one_tile()
+## assert one_tile is not None
+## floor_line.place_tile(one_tile)
+## if cursor.get_held_count(True) == 0:
+## self.game.next_turn()
+## elif self.is_wall_tiling and obj == "Board" and not self.just_dropped:
+## # Mouse down, something pressed, and not holding anything
+## # Wall tiling, pressed, not holding
+## _column_number, row_number = point
+## tile = board.get_tile_for_cursor_by_row(
+## row_number,
+## )
+## if tile is not None:
+## cursor.drag([tile])
+## self.just_held = True
+## if self.is_wall_tiling and self.done_wall_tiling():
+## self.next_round()
+## self.game.next_turn()
+## self.set_attr_all("hidden", self.hidden)
+## super().process(time_passed)
class HaltState(AsyncState["AzulClient"]):
@@ -2523,7 +2273,7 @@ def add_button(
size: int = fontsize,
minlen: int = button_minimum,
) -> int:
- """Add a new Button object to group"""
+ """Add a new Button object to group."""
button = KwargButton(
name,
font=pygame.font.Font(FONT, size),
@@ -2660,6 +2410,54 @@ async def check_conditions(self) -> str:
return "title"
+class InitializeState(GameState):
+ """Initialize state."""
+
+ __slots__ = ()
+
+ def __init__(self) -> None:
+ """Initialize self."""
+ super().__init__("initialize")
+
+ async def entry_actions(self) -> None:
+ """Set up buttons."""
+ assert self.machine is not None
+ self.id = self.machine.new_group("initialize")
+
+ self.group_add(Cursor())
+ await self.manager.raise_event(Event("cursor_drag", [3, 5]))
+ self.manager.register_handler("PygameMouseMotion", self.mouse_moved)
+
+ ## board = Board()
+ #### board.place_tile((2, 2), Tile.red)
+ ## board.location = Vector2.from_iter(SCREEN_SIZE) // 2
+ ## self.group_add(board)
+
+ center = TableCenter()
+ center.location = Vector2.from_iter(SCREEN_SIZE) // 2
+ self.group_add(center)
+ center.add_tiles((0, 1, 2, 3, 5))
+
+ async def mouse_moved(
+ self,
+ event: Event[sprite.PygameMouseMotion],
+ ) -> None:
+ ## print(f'{event = }')
+ await self.manager.raise_event(
+ Event("cursor_set_location", event.data["pos"]),
+ )
+
+
+## await self.manager.raise_event(
+## Event("cursor_set_destination", event.data["pos"]),
+## )
+
+
+## async def check_conditions(self) -> str:
+## """Go to title state."""
+## return "title"
+
+
class TitleState(MenuState):
"""Game state when the title screen is up."""
@@ -2688,7 +2486,7 @@ async def entry_actions(self) -> None:
)
title_text.location = (SCREEN_SIZE[0] // 2, title_text.rect.h)
self.group_add(title_text)
-
+
hosting_button = KwargButton(
"hosting_button",
button_font,
@@ -2847,9 +2645,9 @@ def host_text(x: object) -> str:
size=int(self.fontsize / 1.5),
)
-## # TEMPORARY: Hide everything to do with "Host Mode", networked games aren't done yet.
-## assert self.game is not None
-## self.game.set_attr_all("visible", False)
+ ## # TEMPORARY: Hide everything to do with "Host Mode", networked games aren't done yet.
+ ## assert self.game is not None
+ ## self.game.set_attr_all("visible", False)
def varient_text(x: object) -> str:
return f"Variant Play: {x}"
@@ -3015,7 +2813,6 @@ def exit_actions(self) -> None:
self.game.player_turn = nturn
-
class PhasePrepareNext(GameState):
"""Prepare next phase of game."""
@@ -3052,8 +2849,6 @@ def check_state(self) -> str:
return "End"
-
-
class EndScreen(MenuState):
"""End screen state."""
@@ -3172,8 +2967,6 @@ def entry_actions(self) -> None:
y += self.bh
-
-
class Game(ObjectHandler):
"""Game object, contains most of what's required for Azul."""
@@ -3198,10 +2991,10 @@ def __init__(self) -> None:
PhaseWallTiling(),
PhasePrepareNext(),
EndScreen(),
- PhaseFactoryOfferNetworked(),
- PhaseWallTilingNetworked(),
- PhasePrepareNextNetworked(),
- EndScreenNetworked(),
+ ## PhaseFactoryOfferNetworked(),
+ ## PhaseWallTilingNetworked(),
+ ## PhasePrepareNextNetworked(),
+ ## EndScreenNetworked(),
],
)
self.initialized_state = False
@@ -3568,11 +3361,11 @@ async def entry_actions(self) -> None:
self.id = self.machine.new_group("play")
# self.group_add(())
-## gameboard = GameBoard(
-## 45,
-## )
-## gameboard.location = [x // 2 for x in SCREEN_SIZE]
-## self.group_add(gameboard)
+ ## gameboard = GameBoard(
+ ## 45,
+ ## )
+ ## gameboard.location = [x // 2 for x in SCREEN_SIZE]
+ ## self.group_add(gameboard)
await self.machine.raise_event(Event("init", None))
@@ -3603,7 +3396,7 @@ async def exit_actions(self) -> None:
async def handle_game_over(self, event: Event[int]) -> None:
"""Handle game over event."""
winner = event.data
- self.exit_data = (0, f"{PLAYERS[winner]} Won", False)
+ self.exit_data = (0, f"{winner} Won", False)
await self.machine.raise_event_internal(Event("network_stop", None))
@@ -3714,10 +3507,10 @@ async def async_run() -> None:
screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 16, vsync=VSYNC)
pygame.display.set_caption(f"{__title__} v{__version__}")
# pygame.display.set_icon(pygame.image.load('icon.png'))
- pygame.display.set_icon(get_tile_image(Tiles.one, 32))
+ pygame.display.set_icon(get_tile_image(Tile.one, 32))
screen.fill((0xFF, 0xFF, 0xFF))
-## try:
+ ## try:
async with trio.open_nursery() as main_nursery:
event_manager = ExternalRaiseManager(
"checkers",
@@ -3843,15 +3636,15 @@ def cli_run() -> None:
except ExceptionGroup as exc:
print(exc)
exception = traceback.format_exception(exc)
-## raise
-## except BaseException as ex:
-## screenshot_last_frame()
-## # errorbox.errorbox('Error', f'A {type(ex).__name__} Error Has Occored: {", ".join(ex.args)}')
-## raise
+ ## raise
+ ## except BaseException as ex:
+ ## screenshot_last_frame()
+ ## # errorbox.errorbox('Error', f'A {type(ex).__name__} Error Has Occored: {", ".join(ex.args)}')
+ ## raise
finally:
pygame.quit()
if exception is not None:
- print(''.join(exception), file=sys.stderr)
+ print("".join(exception), file=sys.stderr)
if __name__ == "__main__":
diff --git a/src/azul/server.py b/src/azul/server.py
index cfe9eab..2fc6c76 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -55,7 +55,7 @@
ServerBoundEvents,
find_ip,
)
-from azul.state import State, generate_pieces
+from azul.state import State
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable, Iterable
@@ -130,7 +130,7 @@ async def handle_initial_config(
buffer = Buffer()
-## write_position(buffer, board_size)
+ ## write_position(buffer, board_size)
buffer.write_value(StructFormat.UBYTE, 0)
buffer.write_value(StructFormat.UBYTE, player_turn)
@@ -259,7 +259,7 @@ def __init__(self, internal_singleplayer_mode: bool = False) -> None:
super().__init__("GameServer")
self.client_count: int
- self.state: CheckersState = State(self.board_size, {})
+ self.state = State.new_game(0)
self.client_players: dict[int, int] = {}
self.player_selections: dict[int, Pos] = {}
@@ -276,8 +276,6 @@ def bind_handlers(self) -> None:
"server_start": self.start_server,
"network_stop": self.stop_server,
"server_send_game_start": self.handle_server_start_new_game,
- "network->select_piece": self.handle_network_select_piece,
- "network->select_tile": self.handle_network_select_tile,
},
)
@@ -387,8 +385,8 @@ def new_game_init(self) -> None:
self.client_players.clear()
self.player_selections.clear()
- pieces = generate_pieces(*self.board_size)
- self.state = State(self.board_size, pieces)
+ ## pieces = generate_pieces(*self.board_size)
+ self.state = State.new_game(self.client_count)
# Why keep track of another object just to know client ID numbers
# if we already have that with the components? No need!
@@ -437,25 +435,25 @@ async def transmit_playing_as(self) -> None:
async def handle_server_start_new_game(self, event: Event[None]) -> None:
"""Handle game start."""
-## # Delete all pieces from last state (shouldn't be needed but still.)
-## async with trio.open_nursery() as nursery:
-## for piece_pos, _piece_type in self.state.get_pieces():
-## nursery.start_soon(
-## self.raise_event,
-## Event("delete_piece->network", piece_pos),
-## )
+ ## # Delete all pieces from last state (shouldn't be needed but still.)
+ ## async with trio.open_nursery() as nursery:
+ ## for piece_pos, _piece_type in self.state.get_pieces():
+ ## nursery.start_soon(
+ ## self.raise_event,
+ ## Event("delete_piece->network", piece_pos),
+ ## )
# Choose which team plays first
# Using non-cryptographically secure random because it doesn't matter
self.new_game_init()
-## # Send create_piece events for all pieces
-## async with trio.open_nursery() as nursery:
-## for piece_pos, piece_type in self.state.get_pieces():
-## nursery.start_soon(
-## self.raise_event,
-## Event("create_piece->network", (piece_pos, piece_type)),
-## )
+ ## # Send create_piece events for all pieces
+ ## async with trio.open_nursery() as nursery:
+ ## for piece_pos, piece_type in self.state.get_pieces():
+ ## nursery.start_soon(
+ ## self.raise_event,
+ ## Event("create_piece->network", (piece_pos, piece_type)),
+ ## )
await self.transmit_playing_as()
@@ -537,16 +535,16 @@ async def send_spectator_join_packets(
)
with self.temporary_component(private_events_pocket):
with private_events_pocket.temporary_component(client):
-## # Send create_piece events for all pieces
-## async with trio.open_nursery() as nursery:
-## for piece_pos, piece_type in self.state.get_pieces():
-## nursery.start_soon(
-## client.raise_event,
-## Event(
-## "create_piece->network",
-## (piece_pos, piece_type),
-## ),
-## )
+ ## # Send create_piece events for all pieces
+ ## async with trio.open_nursery() as nursery:
+ ## for piece_pos, piece_type in self.state.get_pieces():
+ ## nursery.start_soon(
+ ## client.raise_event,
+ ## Event(
+ ## "create_piece->network",
+ ## (piece_pos, piece_type),
+ ## ),
+ ## )
await client.raise_event(
Event(f"playing_as->network[{client.client_id}]", 255),
@@ -744,79 +742,6 @@ async def handle_action_animations(
else:
raise NotImplementedError(f"Animation for action {name}")
- async def handle_network_select_tile(
- self,
- event: Event[tuple[int, Pos]],
- ) -> None:
- """Handle select tile event from network."""
- client_id, tile_pos = event.data
-
- player = self.client_players.get(client_id, 0xFF)
- if player == 2:
- player = int(self.state.turn)
-
- if not self.players_can_interact:
- print(
- f"{player = } cannot select tile {tile_pos = } because players_can_interact is False",
- )
- return
-
- if player != self.state.turn:
- print(
- f"{player = } cannot select tile {tile_pos = } because it is not their turn.",
- )
- return
-
- piece_pos = self.player_selections.get(player)
- if piece_pos is None:
- print(
- f"{player = } cannot select tile {tile_pos = } because has no selection",
- )
- return
-
- if tile_pos not in self.state.calculate_actions(piece_pos).ends:
- print(
- f"{player = } cannot select tile {piece_pos!r} because not valid move",
- )
- return
-
- self.players_can_interact = False # No one moves during animation
- # Send animation state start event
- await self.raise_event(Event("animation_state->network", True))
-
- # Remove tile sprites and glowing effect
- await self.player_select_piece(player, None)
-
- action = self.state.action_from_points(piece_pos, tile_pos)
- # print(f"{action = }")
-
- # Get new state after performing valid action
- new_state = self.state.preform_action(action)
- # Get action queue from old state
- action_queue = self.state.get_action_queue()
- self.state = new_state
-
- # Send action animations
- await self.handle_action_animations(action_queue)
-
- # Send action complete event
- await self.raise_event(
- Event(
- "action_complete->network",
- (piece_pos, tile_pos, self.state.turn),
- ),
- )
-
- win_value = self.state.check_for_win()
- if win_value is not None:
- # If we have a winner, send game over event.
- await self.raise_event(Event("game_over->network", win_value))
- return
-
- # If not game over, allow interactions so next player can take turn
- self.players_can_interact = True
- await self.raise_event(Event("animation_state->network", False))
-
def __del__(self) -> None:
"""Debug print."""
print(f"del {self.__class__.__name__}")
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index f98e611..1335e74 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -429,7 +429,7 @@ async def tick(self, tick_event: Event[TickEventData]) -> None:
await trio.lowlevel.checkpoint()
passed = tick_event.data.time_passed
- new = None
+ new: int | str | None = None
if self.update_every == 0:
new = self.fetch_controller_new_state()
else:
@@ -501,6 +501,9 @@ def move_heading_time(self, time_passed: float) -> None:
class TargetingComponent(Component):
"""Sprite that moves toward a destination and then stops.
+ Registered Component Name:
+ targeting
+
Requires components:
Sprite
MovementComponent
@@ -526,6 +529,7 @@ def update_heading(self) -> None:
"""Update the heading of the movement component."""
movement = cast(MovementComponent, self.get_component("movement"))
to_dest = self.to_destination()
+ # If magnitude is zero
if to_dest @ to_dest == 0:
movement.heading = Vector2(0, 0)
return
@@ -555,6 +559,7 @@ def to_destination(self) -> Vector2:
async def move_destination_time(self, time_passed: float) -> None:
"""Move with time_passed."""
if self.__reached:
+ await trio.lowlevel.checkpoint()
return
sprite, movement = cast(
@@ -566,16 +571,25 @@ async def move_destination_time(self, time_passed: float) -> None:
self.__reached = True
await self.raise_event(Event(self.event_raise_name, None))
return
- await trio.lowlevel.checkpoint()
+ to_destination = self.to_destination()
travel_distance = min(
- self.to_destination().magnitude(),
+ to_destination @ to_destination,
movement.speed * time_passed,
)
if travel_distance > 0:
movement.move_heading_distance(travel_distance)
- self.update_heading() # Fix imprecision
+ # Fix imprecision
+ self.update_heading()
+ await trio.lowlevel.checkpoint()
+
+ async def move_destination_time_ticks(
+ self,
+ event: Event[TickEventData],
+ ) -> None:
+ """Move with tick data."""
+ await self.move_destination_time(event.data.time_passed)
class DragEvent(NamedTuple):
diff --git a/src/azul/state.py b/src/azul/state.py
index 114a6a9..05236ff 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -1,10 +1,10 @@
-"""Checkers State."""
+"""Azul State."""
# Programmed by CoolCat467
from __future__ import annotations
-# Copyright (C) 2023-2024 CoolCat467
+# Copyright (C) 2024 CoolCat467
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,443 +19,1042 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-__title__ = "Checkers State"
+__title__ = "Azul State"
__author__ = "CoolCat467"
__license__ = "GNU General Public License Version 3"
__version__ = "0.0.0"
-import copy
-import math
-from dataclasses import dataclass
+
+import random
+from collections import Counter
+from enum import IntEnum, auto
from typing import (
TYPE_CHECKING,
+ Any,
+ Final,
NamedTuple,
- TypeAlias,
TypeVar,
- cast,
)
-from mypy_extensions import u8
+from numpy import array, int8
if TYPE_CHECKING:
- from collections.abc import Callable, Generator, Iterable
+ from collections.abc import Generator
+ from numpy.typing import NDArray
from typing_extensions import Self
-MANDATORY_CAPTURE = True # If a jump is available, do you have to or not?
-PAWN_JUMP_FORWARD_ONLY = True # Pawns not allowed to go backwards in jumps?
+T = TypeVar("T")
-# Note: Tile Ids are chess board tile titles, A1 to H8
-# A8 ... H8
-# .........
-# A1 ... H1
+FLOOR_LINE_COUNT: Final = 7
-# Player:
-# 0 = False = Red = MIN = 0, 2
-# 1 = True = Black = MAX = 1, 3
-T = TypeVar("T")
+def floor_line_subtract_generator(seed: int = 1) -> Generator[int, None, None]:
+ """Floor Line subtraction number generator. Can continue indefinitely."""
+ while True:
+ yield from (-seed,) * (seed + 1)
+ seed += 1
+
+
+FLOOR_LINE_DATA: Final = tuple(
+ value
+ for _, value in zip(
+ range(FLOOR_LINE_COUNT),
+ floor_line_subtract_generator(),
+ strict=False,
+ )
+)
+
+
+class Tile(IntEnum):
+ """All type types."""
+
+ blank = -6
+ fake_cyan = -5
+ fake_black = -4
+ fake_red = -3
+ fake_yellow = -2
+ fake_blue = -1
+ blue = 0
+ yellow = auto()
+ red = auto()
+ black = auto()
+ cyan = auto()
+ one = auto()
+
+
+REAL_TILES: Final = {Tile.blue, Tile.yellow, Tile.red, Tile.black, Tile.cyan}
+
+
+class Phase(IntEnum):
+ """Game phases."""
+
+ factory_offer = 0
+ wall_tiling = auto()
+ end = auto()
+
+
+def generate_bag_contents() -> Counter[int]:
+ """Generate and return unrandomized bag."""
+ tile_types = 5
+ tile_count = 100
+ count_each = tile_count // tile_types
+ return Counter({type_: count_each for type_ in range(tile_types)})
-Pos: TypeAlias = tuple[u8, u8]
-
-
-class Action(NamedTuple):
- """Represents an action."""
-
- from_pos: Pos
- to_pos: Pos
-
-
-class ActionSet(NamedTuple):
- """Represents a set of actions."""
-
- jumps: dict[Pos, list[Pos]]
- moves: tuple[Pos, ...]
- ends: set[Pos]
-
-
-def get_sides(xy: Pos) -> tuple[Pos, Pos, Pos, Pos]:
- """Return the tile xy coordinates on the top left, top right, bottom left, and bottom right sides of given xy coordinates."""
- cx, cy = xy
- sides = []
- for raw_dy in range(2):
- dy = raw_dy * 2 - 1
- ny = cy + dy
- for raw_dx in range(2):
- dx = raw_dx * 2 - 1
- nx = cx + dx
- sides.append((nx, ny))
- tuple_sides = tuple(sides)
- assert len(tuple_sides) == 4
- return cast(tuple[Pos, Pos, Pos, Pos], tuple_sides)
-
-
-def pawn_modify(moves: tuple[T, ...], piece_type: u8) -> tuple[T, ...]:
- """Return moves but remove invalid moves for pawns."""
- assert (
- len(moves) == 4
- ), "Tuple size MUST be four for this to return valid results!"
- if (
- piece_type == 0
- ): # If it's a white pawn, it can only move to top left and top right
- return moves[:2]
- if (
- piece_type == 1
- ): # If it's a black pawn, it can only move to bottom left anf bottom right
- return moves[2:]
- return moves
-
-
-@dataclass(slots=True)
-class State:
- """Represents state of checkers game."""
-
- size: tuple[int, int]
- pieces: dict[Pos, int]
- turn: bool = True # Black moves first
-
- def __str__(self) -> str:
- """Return text representation of game board state."""
- map_ = {None: " ", 0: "-", 1: "+", 2: "O", 3: "X"}
- w, h = self.size
- lines = []
- for y in range(h):
- line = []
- for x in range(w):
- if (x + y + 1) % 2:
- # line.append("_")
- line.append(" ")
- continue
- line.append(map_[self.pieces.get((x, y))])
- lines.append("".join(line))
- # lines.append(" | ".join(line))
- # lines.append("--+-"*(w-1)+"-")
- return "\n".join(lines)
-
- def calculate_actions(self, position: Pos) -> ActionSet:
- """Return actions the piece at given position can make."""
- if MANDATORY_CAPTURE:
- exists = False
- for start, _end in self.get_all_actions(self.pieces[position]):
- if start == position:
- exists = True
- break
- if not exists:
- return ActionSet({}, (), set())
- jumps = self.get_jumps(position)
- moves: tuple[Pos, ...]
- moves = () if MANDATORY_CAPTURE and jumps else self.get_moves(position)
- ends = set(jumps)
- ends.update(moves)
- return ActionSet(jumps, moves, ends)
-
- def piece_kinged(self, piece_pos: Pos, new_type: int) -> None:
- """Piece kinged."""
- # print(f'piece_kinged {piece = }')
-
- def piece_moved(self, start_pos: Pos, end_pos: Pos) -> None:
- """Piece moved from start_pos to end_pos."""
-
- def piece_jumped(self, jumped_piece_pos: Pos) -> None:
- """Piece has been jumped."""
- # print(f'piece_jumped {position = }')
-
- def preform_action(self, action: Action) -> Self:
- """Return new state after performing action on self."""
- from_pos, to_pos = action
-
- pieces_copy = dict(self.pieces.items())
-
- # Remove piece from it's start position
- piece_type = pieces_copy.pop(from_pos)
-
- # See if it's a jump
- if to_pos not in self.get_moves(from_pos):
- # Jumps are more complex to calculate and we need
- # to know what pieces got jumped over
- cur_x, cur_y = from_pos
- for jumped_pos in self.get_jumps(from_pos)[to_pos]:
- from_pos = (cur_x, cur_y)
-
- # Remove jumped position from pieces in play
- if jumped_pos in pieces_copy:
- pieces_copy.pop(jumped_pos)
- self.piece_jumped(jumped_pos)
- # See if piece kinged
- jumped_x, jumped_y = jumped_pos
- # Rightshift 1 is more efficiant way to multiply by 2
- cur_x += (jumped_x - cur_x) << 1
- cur_y += (jumped_y - cur_y) << 1
-
- self.piece_moved(from_pos, (cur_x, cur_y))
-
- # Now that we know the current position, see if kinged
- if self.does_piece_king(piece_type, (cur_x, cur_y)):
- piece_type += 2
- self.piece_kinged((cur_x, cur_y), piece_type)
- else:
- self.piece_moved(from_pos, to_pos)
- # See if it kings and king it if so
- if self.does_piece_king(piece_type, to_pos):
- piece_type += 2
- self.piece_kinged(to_pos, piece_type)
+def bag_draw_tile(bag: Counter[int]) -> int:
+ """Return drawn tile from bag. Mutates bag."""
+ tile = random.choice(tuple(bag.elements()))
+ bag[tile] -= 1
+ return tile
- # Move piece to it's end position
- pieces_copy[to_pos] = piece_type
- # Swap turn
- return self.__class__(
- self.size,
- pieces_copy,
- not self.turn,
+def select_color(holder: Counter[int], color: int) -> int:
+ """Pop color tiles from bag. Returns count. Mutates holder.
+
+ Raises KeyError if color not in holder.
+ """
+ return holder.pop(color)
+
+
+class PatternLine(NamedTuple):
+ """Player pattern line row."""
+
+ color: Tile
+ count_: int
+
+ @classmethod
+ def blank(cls) -> Self:
+ """Return new blank pattern line."""
+ return cls(
+ color=Tile.blank,
+ count_=0,
+ )
+
+ def place_tiles(self, color: Tile, place_count: int) -> Self:
+ """Return new pattern line after placing tiles of given color."""
+ assert self.color == Tile.blank or self.color == color
+ assert place_count > 0
+ return self._replace(
+ color=color,
+ count_=self.count_ + place_count,
)
- def get_tile_name(self, x: int, y: int) -> str:
- """Return name of a given tile."""
- return chr(65 + x) + str(self.size[1] - y)
- @staticmethod
- def action_from_points(start: Pos, end: Pos) -> Action:
- """Return action from given start and end coordinates."""
- # return Action(self.get_tile_name(*start), self.get_tile_name(*end))
- return Action(start, end)
-
- def get_turn(self) -> int:
- """Return whose turn it is. 0 = red, 1 = black."""
- return int(self.turn)
-
- def valid_location(self, position: Pos) -> bool:
- """Return if position is valid."""
- x, y = position
- w, h = self.size
- return x >= 0 and y >= 0 and x < w and y < h
-
- def does_piece_king(self, piece_type: int, position: Pos) -> bool:
- """Return if piece needs to be kinged given it's type and position."""
- _, y = position
- _, h = self.size
- return (piece_type == 0 and y == 0) or (piece_type == 1 and y == h - 1)
+def remove_counter_zeros(counter: Counter[Any]) -> None:
+ """Remove any zero counts from given counter. Mutates counter."""
+ for key, count in tuple(counter.items()):
+ if count == 0:
+ del counter[key]
+
+
+def floor_fill_tile_excess(
+ floor: Counter[int],
+ tile: int,
+ count: int,
+) -> Counter[int]:
+ """Fill floor with count of tile, return excess for box lid. Mutates floor."""
+ excess: Counter[int] = Counter()
+ while floor.total() < FLOOR_LINE_COUNT and count > 0:
+ floor[tile] += 1
+ count -= 1
+ # If overflow and it's number one tile
+ if count and tile == Tile.one:
+ # Move non-one tiles from floor to excess
+ non_one = floor.total() - floor[Tile.one]
+ assert non_one > 0
+ for _ in range(min(non_one, count)):
+ non_one_tiles = set(floor.elements()) - {Tile.one}
+ non_one_tile = sorted(non_one_tiles).pop()
+ # Move non-one tile from floor to box lid
+ floor[non_one_tile] -= 1
+ excess[non_one_tile] += 1
+ # Add one tile to floor
+ floor[tile] += 1
+ count -= 1
+ remove_counter_zeros(floor)
+ assert count >= 0
+ if count:
+ # Add overflow tiles to box lid.
+ excess[tile] += count
+
+ return excess
+
+
+class PlayerData(NamedTuple):
+ """Player data."""
+
+ score: int
+ wall: NDArray[int8]
+ lines: tuple[PatternLine, ...]
+ floor: Counter[int]
+
+ @classmethod
+ def new(cls, varient_play: bool = False) -> Self:
+ """Return new player data instance."""
+ wall = array(
+ [Tile.blank for _ in range(5 * 5)],
+ int8,
+ ).reshape((5, 5))
+
+ if not varient_play:
+ for y in range(5):
+ for x in range(5):
+ color = -((5 - y + x) % len(REAL_TILES) + 1)
+ wall[y, x] = color
+
+ return cls(
+ score=0,
+ wall=wall,
+ lines=(PatternLine.blank(),) * 5,
+ floor=Counter(),
+ )
+
+ def copy(self) -> Self:
+ """Return copy of self."""
+ return self._replace(
+ floor=self.floor.copy(),
+ )
+
+ def line_id_valid(self, line_id: int) -> bool:
+ """Return if given line id is valid."""
+ return line_id >= 0 and line_id < len(self.lines)
@staticmethod
- def get_enemy(self_type: int) -> int:
- """Return enemy pawn piece type."""
- # If we are kinged, get a pawn version of ourselves.
- # Take that plus one mod 2 to get the pawn of the enemy
- return (self_type + 1) % 2
+ def get_line_max_count(line_id: int) -> int:
+ """Return max count allowed in given line."""
+ # Line id is keeping track of max count
+ return line_id + 1
+
+ def get_line_max_placable_count(self, line_id: int) -> int:
+ """Return max placable count for given line."""
+ assert self.line_id_valid(line_id)
+ max_count = self.get_line_max_count(line_id)
+ return max_count - self.lines[line_id].count_
+
+ def get_row_colors_used(self, line_id: int) -> set[Tile]:
+ """Return set of tile colors used in wall for given row."""
+ row = self.wall[line_id, :]
+ return {Tile(int(x)) for x in row[row >= 0]}
+
+ def get_row_unused_colors(self, line_id: int) -> set[Tile]:
+ """Return set of tiles colors not currently used in wall for given row."""
+ return REAL_TILES - self.get_row_colors_used(line_id)
+
+ def yield_possible_placement_rows(
+ self,
+ color: int,
+ ) -> Generator[tuple[int, int], None, None]:
+ """Yield row line ids and number of placable for rows able to place color at."""
+ for line_id, line in enumerate(self.lines):
+ # Color must match
+ if line.color != Tile.blank and int(line.color) != color:
+ # print("color mismatch")
+ continue
+ placable = self.get_line_max_placable_count(line_id)
+ # Must have placable spots
+ if not placable:
+ continue
+ # Must not already use color
+ if color in self.get_row_colors_used(line_id):
+ continue
+ yield (line_id, placable)
+
+ def can_select_line(
+ self,
+ line_id: int,
+ color: int,
+ place_count: int,
+ ) -> bool:
+ """Return if can select given line with given color and place count."""
+ if not self.line_id_valid(line_id):
+ # print("invalid line id")
+ return False
+ line = self.lines[line_id]
+ # Don't allow placing zero
+ if place_count <= 0:
+ # print("place count too smol")
+ return False
+ # Color must match
+ if line.color != Tile.blank and int(line.color) != color:
+ # print("color mismatch")
+ return False
+ # Must have space to place
+ if place_count > self.get_line_max_placable_count(line_id):
+ return False
+ # Can't place in row that uses that color already
+ return Tile(color) not in self.get_row_colors_used(line_id)
@staticmethod
- def get_piece_types(self_type: int) -> tuple[int, int]:
- """Return piece types of given piece type."""
- # If we are kinged, get a pawn version of ourselves.
- self_pawn = self_type % 2
- return (self_pawn, self_pawn + 2)
+ def replace_pattern_line(
+ lines: tuple[PatternLine, ...],
+ line_id: int,
+ new: PatternLine,
+ ) -> tuple[PatternLine, ...]:
+ """Return new pattern line data after replacing one of them."""
+ left = lines[:line_id]
+ right = lines[line_id + 1 :]
+ return (*left, new, *right)
+
+ def place_pattern_line_tiles(
+ self,
+ line_id: int,
+ color: int,
+ place_count: int,
+ ) -> Self:
+ """Return new player data after placing tiles in a pattern line."""
+ assert self.can_select_line(line_id, color, place_count)
+ line = self.lines[line_id]
+ return self._replace(
+ lines=self.replace_pattern_line(
+ self.lines,
+ line_id,
+ line.place_tiles(Tile(color), place_count),
+ ),
+ )
+
+ def is_floor_line_full(self) -> bool:
+ """Return if floor line is full."""
+ return self.floor.total() >= FLOOR_LINE_COUNT
- def get_jumps(
+ def place_floor_line_tiles(
self,
- position: Pos,
- piece_type: int | None = None,
- _pieces: dict[Pos, int] | None = None,
- _recursion: int = 0,
- ) -> dict[Pos, list[Pos]]:
- """Return valid jumps a piece can make.
-
- position is a xy coordinate tuple pointing to a board position
- that may or may not have a piece on it.
- piece_type is the piece type at position. If not
- given, position must point to a tile with a piece on it
-
- Returns dictionary that maps end positions to
- jumped pieces to get there
- """
- if piece_type is None:
- piece_type = self.pieces[position]
- if _pieces is None:
- _pieces = self.pieces
- _pieces = copy.deepcopy(_pieces)
-
- enemy_pieces = self.get_piece_types(self.get_enemy(piece_type))
-
- # Get the side coordinates of the tile and make them tuples so
- # the scan later works properly.
- sides = get_sides(position)
- # Make a dictionary to find what direction a tile is in if you
- # give it the tile.
- # end position : jumped pieces
-
- # Make a dictionary for the valid jumps and the pieces they jump
- valid: dict[Pos, list[Pos]] = {}
-
- valid_sides: tuple[tuple[int, Pos], ...]
- if PAWN_JUMP_FORWARD_ONLY:
- valid_sides = pawn_modify(
- tuple(enumerate(sides)),
- piece_type,
- )
- else:
- valid_sides = tuple(enumerate(sides))
+ color: int,
+ place_count: int,
+ ) -> tuple[Self, Counter[int]]:
+ """Return new player and excess tiles for box lid."""
+ floor = self.floor.copy()
+ for_box_lid = floor_fill_tile_excess(floor, color, place_count)
+ assert all(x > 0 for x in for_box_lid.values()), for_box_lid
+ return (
+ self._replace(floor=floor),
+ for_box_lid,
+ )
- # For each side tile in the jumpable tiles for this type of piece,
- for direction, side in valid_sides:
- # Make sure side exists
- if not self.valid_location(side):
- continue
- side_piece = _pieces.get(side)
- # Side piece must be one of our enemy's pieces
- if side_piece not in enemy_pieces:
- continue
- # Get the direction from the dictionary we made earlier
- # Get the coordinates of the tile on the side of the main tile's
- # side in the same direction as the main tile's side
- side_side = get_sides(side)[direction]
- # Make sure side exists
- if not self.valid_location(side_side):
+ def get_horizontal_linked_wall_count(
+ self,
+ x: int,
+ y: int,
+ wall: NDArray[int8],
+ ) -> int:
+ """Return horizontally-linked tile count."""
+ count = 0
+ for range_ in (range(x - 1, -1, -1), range(x + 1, 5)):
+ for cx in range_:
+ if wall[y, cx] < 0:
+ break
+ count += 1
+ return count
+
+ def get_vertically_linked_wall_count(
+ self,
+ x: int,
+ y: int,
+ wall: NDArray[int8],
+ ) -> int:
+ """Return vertically-linked tile count."""
+ count = 0
+ for range_ in (range(y - 1, -1, -1), range(y + 1, 5)):
+ for cy in range_:
+ if wall[cy, x] < 0:
+ break
+ count += 1
+ return count
+
+ def get_score_from_wall_placement(
+ self,
+ color: int,
+ x: int,
+ y: int,
+ wall: NDArray[int8],
+ ) -> int:
+ """Return score increment value from placing tile at given coordinates."""
+ # Should be blank or fake at position
+ assert wall[y, x] < 0
+ count = 1
+ count += self.get_horizontal_linked_wall_count(x, y, wall)
+ count += self.get_vertically_linked_wall_count(x, y, wall)
+ return count
+
+ def perform_floor_line_scoring(self) -> int:
+ """Return score increment value from floor line."""
+ total_count = self.floor.total()
+ assert total_count <= FLOOR_LINE_COUNT
+ score = 0
+ for idx in range(total_count):
+ score += FLOOR_LINE_DATA[idx]
+ return score
+
+ def perform_auto_wall_tiling(self) -> tuple[Self, Counter[int], bool]:
+ """Return new player data and tiles for box lid after performing automatic wall tiling."""
+ for_box_lid: Counter[int] = Counter()
+
+ score = self.score
+ new_lines = self.lines
+ new_wall = self.wall.copy()
+ for line_id, line in enumerate(self.lines):
+ if line.count_ != self.get_line_max_count(line_id):
continue
- side_side_piece = _pieces.get(side_side)
- # If the side is open,
- if side_side_piece is None:
- # Add it the valid jumps dictionary and add the tile
- # to the list of end tiles.
- valid[side_side] = [side]
-
- # Remove jumped piece from future calculations
- _pieces.pop(side)
-
- # For each end point tile in the list of end point tiles,
- for end_tile in tuple(valid):
- # Get the dictionary from the jumps you could make
- # from that end tile
- w, h = self.size
- if _recursion + 1 > math.ceil((w**2 + h**2) ** 0.25):
- break
- # If the piece has made it to the opposite side,
- piece_type_copy = piece_type
- if self.does_piece_king(piece_type_copy, end_tile):
- # King that piece
- piece_type_copy += 2
- _recursion = -1
- add_valid = self.get_jumps(
- end_tile,
- piece_type_copy,
- _pieces=_pieces,
- _recursion=_recursion + 1,
+ right = max(0, line.count_ - 1)
+ if right:
+ for_box_lid[line.color] += right
+ x = tuple(map(int, new_wall[line_id, :])).index(-line.color - 1)
+ score += self.get_score_from_wall_placement(
+ line.color,
+ x,
+ line_id,
+ new_wall,
)
- # For each key in the new dictionary of valid tile's keys,
- for end_pos, jumped_pieces in add_valid.items():
- # If the key is not already existent in the list of
- # valid destinations,
- if end_pos not in valid:
- # Add that destination to the dictionary and every
- # tile you have to jump to get there.
- no_duplicates = [
- p for p in jumped_pieces if p not in valid[end_tile]
- ]
- valid[end_pos] = valid[end_tile] + no_duplicates
-
- return valid
-
- def get_moves(self, position: Pos) -> tuple[Pos, ...]:
- """Return valid moves piece at position can make, not including jumps."""
- piece_type = self.pieces[position]
- # Get the side xy choords of the tile's xy pos,
- # then modify results for pawns
- moves = pawn_modify(get_sides(position), piece_type)
- return tuple(
- m
- for m in filter(self.valid_location, moves)
- if m not in self.pieces
+ new_wall[line_id, x] = line.color
+ new_lines = self.replace_pattern_line(
+ new_lines,
+ line_id,
+ PatternLine.blank(),
+ )
+
+ score += self.perform_floor_line_scoring()
+ if score < 0:
+ score = 0
+
+ # Get one tile from floor line
+ floor = self.floor.copy()
+ has_one = False
+ if floor[Tile.one]:
+ floor[Tile.one] -= 1
+ remove_counter_zeros(floor)
+ has_one = True
+ for_box_lid.update(floor)
+
+ return (
+ self._replace(
+ lines=new_lines,
+ wall=new_wall,
+ score=score,
+ floor=Counter(),
+ ),
+ for_box_lid,
+ has_one,
)
+ def has_horizontal_wall_line(self) -> bool:
+ """Return if full horizontal line is filled anywhere."""
+ return any(all(self.wall[y, :] >= 0) for y in range(5))
+
+ def get_filled_horizontal_line_count(self) -> int:
+ """Return number of filled horizontal lines."""
+ count = 0
+ for y in range(5):
+ if all(self.wall[y, :] >= 0):
+ count += 1
+ return count
+
+ def get_end_of_game_score(self) -> int:
+ """Return end of game score for this player."""
+ score = self.score
+ score += self.get_filled_horizontal_line_count() * 2
+ for x in range(5):
+ if all(self.wall[:, x] >= 0):
+ score += 7
+ counts = Counter(int(x) for x in self.wall[self.wall >= 0])
+ for count in counts.values():
+ if count == 5:
+ score += 10
+ return score
+
+ def perform_end_of_game_scoring(self) -> Self:
+ """Return new player data after performing end of game scoring."""
+ return self._replace(score=self.get_end_of_game_score())
+
+
+def factory_displays_deepcopy(
+ factory_displays: dict[int, Counter[int]],
+) -> dict[int, Counter[int]]:
+ """Return deepcopy of factory displays."""
+ return {k: v.copy() for k, v in factory_displays.items()}
+
+
+def player_data_deepcopy(
+ player_data: dict[int, PlayerData],
+) -> dict[int, PlayerData]:
+ """Return deepcopy of player data."""
+ return {k: v.copy() for k, v in player_data.items()}
+
+
+class SelectableSource(IntEnum):
+ """Selectable tile source."""
+
+ table_center = 0
+ factory = auto()
+
+
+class SelectableSourceTiles(NamedTuple):
+ """Selectable source tiles data."""
+
+ source: SelectableSource
+ tiles: Tile
+ # Factory ids
+ source_id: int | None = None
+
+
+class SelectableDestination(IntEnum):
+ """Selectable tile destination."""
+
+ floor_line = 0
+ pattern_line = auto()
+
+
+class SelectableDestinationTiles(NamedTuple):
+ """Selectable destination tiles data."""
+
+ destination: SelectableDestination
+ place_count: int
+ # Pattern line ids
+ destination_id: int | None = None
+
+
+class State(NamedTuple):
+ """Represents state of an azul game."""
+
+ varient_play: bool
+ current_phase: Phase
+ bag: Counter[int]
+ box_lid: Counter[int]
+ table_center: Counter[int]
+ factory_displays: dict[int, Counter[int]]
+ cursor_contents: Counter[int]
+ current_turn: int
+ player_data: dict[int, PlayerData]
+
@classmethod
- def wrap_actions(
- cls,
- position: Pos,
- calculate_ends: Callable[[Pos], Iterable[Pos]],
- ) -> Generator[Action, None, None]:
- """Yield end calculation function results as Actions."""
- for end in calculate_ends(position):
- yield cls.action_from_points(position, end)
-
- def get_actions(self, position: Pos) -> Generator[Action, None, None]:
- """Yield all moves and jumps the piece at position can make."""
- ends = set(self.get_jumps(position))
- if not (ends and MANDATORY_CAPTURE):
- ends.update(self.get_moves(position))
- for end in ends:
- yield self.action_from_points(position, end)
-
- def get_all_actions(self, player: int) -> Generator[Action, None, None]:
- """Yield all actions for given player."""
- player_pieces = {player, player + 2}
- if not MANDATORY_CAPTURE:
- for position, piece_type in self.pieces.items():
- if piece_type not in player_pieces:
- continue
- yield from self.get_actions(position)
- return
- jumps_available = False
- for position, piece_type in self.pieces.items():
- if piece_type not in player_pieces:
+ def new_game(cls, player_count: int, varient_play: bool = False) -> Self:
+ """Return state of a new game."""
+ factory_count = player_count * 2 + 1
+ bag = generate_bag_contents()
+
+ factory_displays: dict[int, Counter[int]] = {}
+ for x in range(factory_count):
+ tiles: Counter[int] = Counter()
+ for _ in range(4):
+ tiles[bag_draw_tile(bag)] += 1
+ factory_displays[x] = tiles
+
+ return cls(
+ varient_play=varient_play,
+ current_phase=Phase.factory_offer,
+ bag=bag,
+ box_lid=Counter(),
+ table_center=Counter({Tile.one, 1}),
+ factory_displays=factory_displays,
+ cursor_contents=Counter(),
+ current_turn=0,
+ player_data={
+ x: PlayerData.new(varient_play) for x in range(player_count)
+ },
+ )
+
+ def is_cursor_empty(self) -> bool:
+ """Return if cursor is empty."""
+ return self.cursor_contents.total() == 0
+
+ def can_cursor_select_factory(self, factory_id: int) -> bool:
+ """Return if cursor can select a specific factory."""
+ assert self.current_phase == Phase.factory_offer
+ if not self.is_cursor_empty():
+ return False
+ factory = self.factory_displays.get(factory_id, None)
+ if factory is None:
+ return False
+ return factory.total() > 0
+
+ def can_cursor_select_factory_color(
+ self,
+ factory_id: int,
+ color: int,
+ ) -> bool:
+ """Return if cursor can select color at factory."""
+ if not self.can_cursor_select_factory(factory_id):
+ return False
+ factory = self.factory_displays[factory_id]
+ return factory[color] > 0
+
+ def cursor_selects_factory(self, factory_id: int, color: int) -> Self:
+ """Return new state after cursor selects factory."""
+ assert self.can_cursor_select_factory_color(factory_id, color)
+ # Only mutate copies
+ factory_displays = factory_displays_deepcopy(self.factory_displays)
+ table_center = self.table_center.copy()
+ cursor_contents = self.cursor_contents.copy()
+
+ factory = factory_displays[factory_id]
+ count = select_color(factory, color)
+ # Add to cursor
+ cursor_contents[color] += count
+ # Add all non-matching colored tiles to center of table
+ table_center.update(factory)
+ factory.clear()
+
+ return self._replace(
+ table_center=table_center,
+ factory_displays=factory_displays,
+ cursor_contents=cursor_contents,
+ )
+
+ def can_cursor_select_center(self, color: int) -> bool:
+ """Return if cursor can select color from table center."""
+ assert self.current_phase == Phase.factory_offer
+ if not self.is_cursor_empty():
+ return False
+ return color != Tile.one and self.table_center[color] > 0
+
+ def cursor_selects_table_center(self, color: int) -> Self:
+ """Return new state after cursor selects from table center."""
+ assert self.can_cursor_select_center(color)
+ table_center = self.table_center.copy()
+ cursor_contents = self.cursor_contents.copy()
+
+ # Get all of color from table center and add to cursor
+ cursor_contents[color] += select_color(table_center, color)
+ # Handle number one tile
+ if table_center[Tile.one]:
+ cursor_contents[Tile.one] += select_color(table_center, Tile.one)
+ remove_counter_zeros(table_center)
+
+ return self._replace(
+ table_center=table_center,
+ cursor_contents=cursor_contents,
+ )
+
+ def yield_table_center_selections(
+ self,
+ ) -> Generator[SelectableSourceTiles, None, None]:
+ """Yield SelectableSourceTiles objects from table center."""
+ for color, count in self.table_center.items():
+ if color == Tile.one or count <= 0:
continue
- if not jumps_available:
- for jump in self.wrap_actions(position, self.get_jumps):
- yield jump
- jumps_available = True
- else:
- yield from self.wrap_actions(position, self.get_jumps)
- if not jumps_available:
- for position, piece_type in self.pieces.items():
- if piece_type not in player_pieces:
- continue
- yield from self.wrap_actions(position, self.get_moves)
-
- def check_for_win(self) -> int | None:
- """Return player number if they won else None."""
- # For each of the two players,
- for player in range(2):
- # For each tile in the playable tiles,
- has_move = False
- for _ in self.get_all_actions(player):
- has_move = True
- # Player has at least one move, no need to continue
- break
- if not has_move and self.turn == bool(player):
- # Continued without break, so player either has no moves
- # or no possible moves, so their opponent wins
- return (player + 1) % 2
- return None
-
- def can_player_select_piece(self, player: int, tile_pos: Pos) -> bool:
- """Return True if player can select piece on given tile position."""
- piece_at_pos = self.pieces.get(tile_pos)
- if piece_at_pos is None:
+ yield SelectableSourceTiles(
+ source=SelectableSource.table_center,
+ tiles=Tile(color),
+ )
+
+ def yield_selectable_tiles_factory_offer(
+ self,
+ ) -> Generator[SelectableSourceTiles, None, None]:
+ """Yield SelectableSourceTiles objects from all sources."""
+ yield from self.yield_table_center_selections()
+ for factory_id, factory_display in self.factory_displays.items():
+ for color in factory_display:
+ yield SelectableSourceTiles(
+ source=SelectableSource.factory,
+ tiles=Tile(color),
+ source_id=factory_id,
+ )
+
+ def apply_source_select_action_factory_offer(
+ self,
+ selection: SelectableSourceTiles,
+ ) -> Self:
+ """Return new state after applying selection action."""
+ color = selection.tiles
+ if selection.source == SelectableSource.table_center:
+ return self.cursor_selects_table_center(color)
+ if selection.source == SelectableSource.factory:
+ assert selection.source_id is not None
+ return self.cursor_selects_factory(selection.source_id, color)
+ raise NotImplementedError(selection.source)
+
+ def get_cursor_holding_color(self) -> int:
+ """Return color of tile cursor is holding."""
+ cursor_colors = set(self.cursor_contents.elements())
+ # Do not count number one tile
+ cursor_colors.discard(Tile.one)
+ assert len(cursor_colors) == 1, "Cursor should only exactly one color"
+ return cursor_colors.pop()
+
+ def can_player_select_line(
+ self,
+ line_id: int,
+ color: int,
+ place_count: int,
+ ) -> bool:
+ """Return if player can select line."""
+ player_data = self.player_data[self.current_turn]
+
+ # Cannot place more than we have
+ # Can't be pulling tiles out of thin air now can we?
+ if place_count > self.cursor_contents[color]:
+ return False
+
+ return player_data.can_select_line(line_id, color, place_count)
+
+ def get_player_line_max_placable_count(self, line_id: int) -> int:
+ """Return max placable count for given line."""
+ player_data = self.player_data[self.current_turn]
+
+ return player_data.get_line_max_placable_count(line_id)
+
+ def all_pullable_empty(self) -> bool:
+ """Return if all pullable tile locations are empty, not counting cursor."""
+ if self.table_center.total():
return False
- return (piece_at_pos % 2) == player
-
- def get_pieces(self) -> tuple[tuple[Pos, int], ...]:
- """Return all pieces."""
- return tuple((pos, type_) for pos, type_ in self.pieces.items())
-
-
-def generate_pieces(
- board_width: int,
- board_height: int,
- colors: int = 2,
-) -> dict[Pos, int]:
- """Generate data about each piece."""
- pieces: dict[Pos, int] = {}
- # Get where pieces should be placed
- z_to_1 = round(board_height / 3) # White
- z_to_2 = (board_height - (z_to_1 * 2)) + z_to_1 # Black
- # For each xy position in the area of where tiles should be,
- for y in range(board_height):
- # Reset the x pos to 0
- for x in range(board_width):
- # Get the color of that spot by adding x and y mod the number of different colors
- color = (x + y + 1) % colors
- # If a piece should be placed on that tile and the tile is not Red,
- if (not color) and ((y <= z_to_1 - 1) or (y >= z_to_2)):
- # Set the piece to White Pawn or Black Pawn depending on the current y pos
- piece_type = int(y <= z_to_1)
- pieces[x, y] = piece_type
- return pieces
+ for factory_display in self.factory_displays.values():
+ if factory_display.total():
+ return False
+ return True
+
+ def _factory_offer_maybe_next_turn(self) -> Self:
+ """Return either current state or new state if player's turn is over."""
+ assert self.current_phase == Phase.factory_offer
+ # If cursor is still holding things, turn is not over.
+ if not self.is_cursor_empty():
+ return self
+ # Turn is over
+ # Increment who's turn it is
+ current_turn = (self.current_turn + 1) % len(self.player_data)
+
+ current_phase: Phase = self.current_phase
+ if self.all_pullable_empty():
+ # Go to wall tiling phase
+ current_phase = Phase.wall_tiling
+
+ new_state = self._replace(
+ current_phase=current_phase,
+ current_turn=current_turn,
+ )
+ if current_phase == Phase.wall_tiling:
+ if self.varient_play:
+ raise NotImplementedError()
+ return new_state.apply_auto_wall_tiling()
+ return new_state
+
+ def player_select_floor_line(self, color: int, place_count: int) -> Self:
+ """Return new state after player adds tiles to floor line."""
+ assert self.current_phase == Phase.factory_offer
+ cursor_contents = self.cursor_contents.copy()
+ assert place_count > 0
+ assert place_count <= cursor_contents[color]
+
+ box_lid = self.box_lid.copy()
+ current_player_data = self.player_data[self.current_turn]
+
+ # Remove from cursor
+ cursor_contents[color] -= place_count
+ # Add to floor line
+ new_player_data, for_box_lid = (
+ current_player_data.place_floor_line_tiles(
+ color,
+ place_count,
+ )
+ )
+ # Add overflow tiles to box lid
+ assert all(x > 0 for x in for_box_lid.values()), for_box_lid
+ box_lid.update(for_box_lid)
+
+ # If has number one tile, add to floor line
+ if cursor_contents[Tile.one]:
+ # Add to floor line
+ new_player_data, for_box_lid = (
+ new_player_data.place_floor_line_tiles(
+ Tile.one,
+ cursor_contents.pop(Tile.one),
+ )
+ )
+ # Add overflow tiles to box lid
+ assert all(x > 0 for x in for_box_lid.values()), for_box_lid
+ box_lid.update(for_box_lid)
+
+ remove_counter_zeros(cursor_contents)
+
+ # Update player data
+ player_data = player_data_deepcopy(self.player_data)
+ player_data[self.current_turn] = new_player_data
+
+ return self._replace(
+ box_lid=box_lid,
+ cursor_contents=cursor_contents,
+ player_data=player_data,
+ )._factory_offer_maybe_next_turn()
+
+ def player_selects_pattern_line(
+ self,
+ line_id: int,
+ place_count: int,
+ ) -> Self:
+ """Return new state after player selects line."""
+ assert self.current_phase == Phase.factory_offer
+ assert not self.is_cursor_empty()
+ color = self.get_cursor_holding_color()
+
+ assert self.can_player_select_line(line_id, color, place_count)
+ current_player_data = self.player_data[self.current_turn]
+
+ new_player_data = current_player_data.place_pattern_line_tiles(
+ line_id,
+ color,
+ place_count,
+ )
+
+ cursor_contents = self.cursor_contents.copy()
+ cursor_contents[color] -= place_count
+
+ # Might need to change box lid
+ box_lid = self.box_lid
+
+ # If has number one tile, add to floor line
+ if cursor_contents[Tile.one]:
+ # Will be mutating box lid then
+ box_lid = self.box_lid.copy()
+ # Add to floor line
+ new_player_data, for_box_lid = (
+ new_player_data.place_floor_line_tiles(
+ Tile.one,
+ cursor_contents.pop(Tile.one),
+ )
+ )
+ # Add overflow tiles to box lid
+ assert all(x > 0 for x in for_box_lid.values()), for_box_lid
+ box_lid.update(for_box_lid)
+
+ remove_counter_zeros(cursor_contents)
+
+ player_data = player_data_deepcopy(self.player_data)
+ player_data[self.current_turn] = new_player_data
+
+ return self._replace(
+ box_lid=box_lid,
+ player_data=player_data,
+ cursor_contents=cursor_contents,
+ )._factory_offer_maybe_next_turn()
+
+ def yield_selectable_tile_destinations_factory_offer(
+ self,
+ ) -> Generator[SelectableDestinationTiles, None, None]:
+ """Yield selectable tile destinations for factory offer phase."""
+ assert self.current_phase == Phase.factory_offer
+ assert not self.is_cursor_empty()
+
+ current_player_data = self.player_data[self.current_turn]
+
+ color = self.get_cursor_holding_color()
+ count = self.cursor_contents[color] + 1
+
+ for (
+ line_id,
+ placable,
+ ) in current_player_data.yield_possible_placement_rows(color):
+ for place_count in range(1, min(count, placable + 1)):
+ yield SelectableDestinationTiles(
+ destination=SelectableDestination.pattern_line,
+ place_count=place_count,
+ destination_id=line_id,
+ )
+ # Can always place in floor line, even if full,
+ # because of box lid overflow
+ for place_count in range(1, count):
+ yield SelectableDestinationTiles(
+ destination=SelectableDestination.floor_line,
+ place_count=place_count,
+ )
+
+ def apply_destination_select_action_factory_offer(
+ self,
+ selection: SelectableDestinationTiles,
+ ) -> Self:
+ """Return new state after applying destination selection action."""
+ assert self.current_phase == Phase.factory_offer
+ assert not self.is_cursor_empty()
+
+ if selection.destination == SelectableDestination.floor_line:
+ color = self.get_cursor_holding_color()
+ return self.player_select_floor_line(
+ color,
+ selection.place_count,
+ )
+ if selection.destination == SelectableDestination.pattern_line:
+ assert selection.destination_id is not None
+ return self.player_selects_pattern_line(
+ selection.destination_id,
+ selection.place_count,
+ )
+ raise NotImplementedError(selection.destination)
+
+ def apply_auto_wall_tiling(self) -> Self:
+ """Return new state after performing automatic wall tiling."""
+ assert self.current_phase == Phase.wall_tiling
+ assert not self.varient_play
+ box_lid = self.box_lid.copy()
+ new_players = player_data_deepcopy(self.player_data)
+
+ is_end = False
+ current_turn = self.current_turn
+ for player_id, player in self.player_data.items():
+ new_player, for_box_lid, has_one = (
+ player.perform_auto_wall_tiling()
+ )
+ new_players[player_id] = new_player
+ box_lid.update(for_box_lid)
+ if not is_end:
+ is_end = new_player.has_horizontal_wall_line()
+ if has_one:
+ current_turn = player_id
+
+ bag = self.bag.copy()
+ factory_displays: dict[int, Counter[int]] = {}
+
+ if is_end:
+ for player_id in self.player_data:
+ new_players[player_id] = new_players[
+ player_id
+ ].perform_end_of_game_scoring()
+ else:
+ out_of_tiles = False
+ for factory_id in self.factory_displays:
+ tiles: Counter[int] = Counter()
+ if out_of_tiles:
+ factory_displays[factory_id] = tiles
+ continue
+ for _ in range(4):
+ if bag.total() > 0:
+ tiles[bag_draw_tile(bag)] += 1
+ else:
+ bag = box_lid
+ box_lid = Counter()
+ if bag.total() <= 0:
+ # "In the rare case that you run out of
+ # tiles again while there are one left in
+ # the lid, start the new round as usual even
+ # though not all Factory displays are
+ # properly filled."
+ out_of_tiles = True
+ break
+ factory_displays[factory_id] = tiles
+
+ return self._replace(
+ current_phase=Phase.end if is_end else Phase.factory_offer,
+ current_turn=current_turn,
+ player_data=new_players,
+ bag=bag,
+ box_lid=box_lid,
+ factory_displays=factory_displays,
+ table_center=Counter({Tile.one: 1}),
+ )
+
+ def get_win_order(self) -> list[tuple[int, int]]:
+ """Return player ranking with (id, compare_score) entries."""
+ counts: dict[int, int] = {}
+ # get_filled_horizontal_line_count can return at most 5
+ for player_id, player in self.player_data.items():
+ counts[player_id] = (
+ player.score * 6 + player.get_filled_horizontal_line_count()
+ )
+ return sorted(counts.items(), key=lambda x: x[1], reverse=True)
+
+ def yield_all_factory_offer_destinations(
+ self,
+ ) -> Generator[tuple[SelectableDestinationTiles, ...]]:
+ """Yield all factory offer destinations."""
+ if self.is_cursor_empty():
+ yield ()
+ else:
+ for (
+ destination
+ ) in self.yield_selectable_tile_destinations_factory_offer():
+ new = self.apply_destination_select_action_factory_offer(
+ destination,
+ )
+ did_not_iter = True
+ for action in new.yield_all_factory_offer_destinations():
+ did_not_iter = False
+ yield (destination, *action)
+ if did_not_iter:
+ yield (destination,)
+
+ def yield_actions(
+ self,
+ ) -> Generator[
+ tuple[SelectableDestinationTiles, ...]
+ | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]],
+ None,
+ None,
+ ]:
+ """Yield all possible actions able to be performed on this state."""
+ if self.current_phase == Phase.factory_offer:
+ if not self.is_cursor_empty():
+ yield from self.yield_all_factory_offer_destinations()
+ else:
+ for selection in self.yield_selectable_tiles_factory_offer():
+ new = self.apply_source_select_action_factory_offer(
+ selection,
+ )
+ for (
+ action_chain
+ ) in new.yield_all_factory_offer_destinations():
+ yield (selection, action_chain)
+ else:
+ raise NotImplementedError()
+
+ def preform_action(
+ self,
+ action: (
+ tuple[SelectableDestinationTiles, ...]
+ | tuple[
+ SelectableSourceTiles,
+ tuple[SelectableDestinationTiles, ...],
+ ]
+ ),
+ ) -> Self:
+ """Return new state after applying an action."""
+ if self.current_phase == Phase.factory_offer:
+ if isinstance(action[0], SelectableDestinationTiles):
+ new = self
+ for destination in action:
+ assert isinstance(destination, SelectableDestinationTiles)
+ new = new.apply_destination_select_action_factory_offer(
+ destination,
+ )
+ return new
+ selection, destinations = action
+ assert isinstance(selection, SelectableSourceTiles)
+ new = self.apply_source_select_action_factory_offer(
+ selection,
+ )
+ for destination_ in destinations:
+ assert isinstance(destination_, SelectableDestinationTiles)
+ new = new.apply_destination_select_action_factory_offer(
+ destination_,
+ )
+ return new
+ raise NotImplementedError()
+
+
+def run() -> None:
+ """Run program."""
+ from market_api import pretty_print_response as pprint
+
+ random.seed(0)
+ state = State.new_game(2)
+ ticks = 0
+ try:
+ ## last_turn = -1
+ while state.current_phase == Phase.factory_offer:
+ ## assert last_turn != state.current_turn
+ ## last_turn = state.current_turn
+ actions = tuple(state.yield_actions())
+ print(f"{len(actions) = }")
+ action = random.choice(actions)
+ ## pprint(action)
+ state = state.preform_action(action)
+
+ ticks += 1
+ print(f"{state.get_win_order() = }")
+ except BaseException:
+ print(f"{ticks = }")
+ ## print(f'{state = }')
+ pprint(state)
+ raise
+ ## print(f'{destination = }')
+ ## pprint(state)
+ pprint(state)
+
+
+if __name__ == "__main__":
+ run()
diff --git a/src/azul/tools.py b/src/azul/tools.py
index 5c8629f..ccdc9e4 100644
--- a/src/azul/tools.py
+++ b/src/azul/tools.py
@@ -3,14 +3,7 @@
from __future__ import annotations
# Programmed by CoolCat467
-import math
-import random
-from typing import TYPE_CHECKING, TypeVar
-
-if TYPE_CHECKING:
- from collections.abc import Generator, Iterable
-
- from azul.game import Tile
+from typing import TypeVar
T = TypeVar("T")
Numeric = TypeVar("Numeric", int, float)
@@ -37,32 +30,6 @@ def saturate(value: Numeric, low: Numeric, high: Numeric) -> Numeric:
return min(max(value, low), high)
-def randomize(iterable: Iterable[T]) -> list[T]:
- """Randomize all values of an iterable."""
- lst = list(iterable)
- random.shuffle(lst)
- return lst
-
-
-def gen_random_proper_seq(length: int, **kwargs: float) -> list[str]:
- """Generate a random sequence of letters given keyword arguments of =."""
- letters = []
- if sum(list(kwargs.values())) != 1:
- raise ArithmeticError(
- "Sum of percentages of "
- + " ".join(list(kwargs.keys()))
- + " are not equal to 100 percent!",
- )
- for letter in kwargs:
- letters += [letter] * math.ceil(length * kwargs[letter])
- return randomize(letters)
-
-
-def sort_tiles(tile_object: Tile) -> int:
- """Key function for sorting tiles."""
- return tile_object.color
-
-
# def getCacheSignatureTile(tile, tilesize, greyshift, outlineSize):
# """Return the string a tile and it's configuration information would use to identify itself in the tile cache."""
# safeFloat = lambda x: round(x*100)
@@ -70,12 +37,3 @@ def sort_tiles(tile_object: Tile) -> int:
# data = tile.color, safeFloat(tilesize), safeFloat(greyshift), safeFloat(outlineSize)
# # types: ^
# return ''.join((str(i) for i in data))
-
-
-def floor_line_subtract_generator(seed: int = 1) -> Generator[int, None, None]:
- """Floor Line subtraction number generator. Can continue indefinitely."""
- num = seed
- while True:
- nxt = [-num] * (num + 1)
- yield from nxt
- num += 1
From 6f3f75b3502268abce40ae9e699cb1c95807f749 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Mon, 25 Nov 2024 01:13:38 -0600
Subject: [PATCH 03/67] Do some small things with state Update minimax module
Remove some checkers code from server
---
.pre-commit-config.yaml | 2 +-
computer_players/MiniMax_AI.py | 21 ++--
computer_players/machine_client.py | 2 +-
computer_players/minimax.py | 143 +------------------------
src/azul/game.py | 3 +-
src/azul/server.py | 165 ++---------------------------
src/azul/state.py | 20 ++--
7 files changed, 43 insertions(+), 313 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5da692c..f030e52 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -23,7 +23,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.7.4
+ rev: v0.8.0
hooks:
- id: ruff
types: [file]
diff --git a/computer_players/MiniMax_AI.py b/computer_players/MiniMax_AI.py
index 6bf8909..bbc0865 100755
--- a/computer_players/MiniMax_AI.py
+++ b/computer_players/MiniMax_AI.py
@@ -14,7 +14,7 @@
from typing import TYPE_CHECKING, TypeAlias, TypeVar
##from machine_client import RemoteState, run_clients_in_local_servers_sync
-from minimax import Minimax, Player
+from minimax import Minimax, MinimaxResult, Player
from azul.state import (
Phase,
@@ -50,13 +50,14 @@ def value(state: State) -> int | float:
if AzulMinimax.terminal(state):
winner, _score = real_state.get_win_order()[0]
if winner == max_player:
- return 1
- return -1
+ return 10
+ return -10
# Heuristic
min_ = 0
max_ = 0
for player_id, player_data in real_state.player_data.items():
score = player_data.get_end_of_game_score()
+ score += player_data.get_floor_line_scoring()
if player_id == max_player:
max_ += score
else:
@@ -64,7 +65,7 @@ def value(state: State) -> int | float:
# More max will make score higher,
# more min will make score lower
# Plus one in divisor makes so never / 0
- return (max_ - min_) / (max_ + min_ + 1)
+ return (max_ - min_) / (abs(max_) + abs(min_) + 1)
@staticmethod
def terminal(state: State) -> bool:
@@ -93,6 +94,13 @@ def result(state: State, action: Action) -> State:
real_state, max_player = state
return (real_state.preform_action(action), max_player)
+ @classmethod
+ def adaptive_depth_minimax(cls, state: State) -> MinimaxResult[Action]:
+ """Adaptive depth minimax."""
+ # TODO
+ depth = 1
+ return cls.alphabeta(state, depth)
+
##class MinimaxPlayer(RemoteState):
## """Minimax Player."""
@@ -122,9 +130,10 @@ def run() -> None:
state = (State.new_game(2), 0)
while not AzulMinimax.terminal(state):
- action = AzulMinimax.alphabeta(state, 2)
- print(f"{action.value = }")
+ action = AzulMinimax.adaptive_depth_minimax(state)
+ print(f"{action = }")
state = AzulMinimax.result(state, action.action)
+ print(f"{state = }")
print(state)
diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py
index dac52f7..78254ce 100644
--- a/computer_players/machine_client.py
+++ b/computer_players/machine_client.py
@@ -39,7 +39,7 @@ class RemoteState(Component, metaclass=ABCMeta):
turn.
"""
- __slots__ = ("state", "pieces", "has_initial", "playing_as", "moves")
+ __slots__ = ("has_initial", "moves", "pieces", "playing_as", "state")
def __init__(self) -> None:
"""Initialize remote state."""
diff --git a/computer_players/minimax.py b/computer_players/minimax.py
index c3a325f..0a4600a 100644
--- a/computer_players/minimax.py
+++ b/computer_players/minimax.py
@@ -44,149 +44,16 @@ class Minimax(ABC, Generic[State, Action]):
__slots__ = ()
- @classmethod
- @abstractmethod
- def value(cls, state: State) -> int | float:
- """Return the value of a given game state."""
-
- @classmethod
- @abstractmethod
- def terminal(cls, state: State) -> bool:
- """Return if given game state is terminal."""
-
- @classmethod
- @abstractmethod
- def player(cls, state: State) -> Player:
- """Return player status given the state of the game.
-
- Must return either Player.MIN or Player.MAX
- """
-
- @classmethod
- @abstractmethod
- def actions(cls, state: State) -> Iterable[Action]:
- """Return a collection of all possible actions in a given game state."""
-
- @classmethod
- @abstractmethod
- def result(cls, state: State, action: Action) -> State:
- """Return new game state after performing action on given state."""
-
- @classmethod
- def minimax(
- cls,
- state: State,
- depth: int | None = 5,
- ) -> MinimaxResult[Action]:
- """Return minimax result best action for a given state for the current player."""
- if cls.terminal(state):
- return MinimaxResult(cls.value(state), None)
- if depth is not None and depth <= 0:
- # Choose a random action
- # No need for cryptographic secure random
- return MinimaxResult(
- cls.value(state),
- random.choice(tuple(cls.actions(state))), # noqa: S311
- )
- next_down = None if depth is None else depth - 1
-
- current_player = cls.player(state)
- value: int | float
- if current_player == Player.MAX:
- value = -infinity
- best = max
- elif current_player == Player.MIN:
- value = infinity
- best = min
- elif current_player == Player.CHANCE:
- raise ValueError("CHANCE is not valid for regular minimax.")
- else:
- raise ValueError(f"Unexpected player type {current_player!r}")
-
- best_action: Action | None = None
- for action in cls.actions(state):
- result = cls.minimax(cls.result(state, action), next_down)
- new_value = best(value, result.value)
- if new_value != value:
- best_action = action
- value = new_value
- return MinimaxResult(value, best_action)
-
- @classmethod
- def alphabeta(
- cls,
- state: State,
- depth: int | None = 5,
- a: int | float = -infinity,
- b: int | float = infinity,
- ) -> MinimaxResult[Action]:
- """Return minimax alphabeta pruning result best action for given current state."""
- # print(f'alphabeta {depth = } {a = } {b = }')
-
- if cls.terminal(state):
- return MinimaxResult(cls.value(state), None)
- if depth is not None and depth <= 0:
- # Choose a random action
- # No need for cryptographic secure random
- return MinimaxResult(
- cls.value(state),
- random.choice(tuple(cls.actions(state))), # noqa: S311
- )
- next_down = None if depth is None else depth - 1
-
- current_player = cls.player(state)
- value: int | float
- if current_player == Player.MAX:
- value = -infinity
- best = max
- compare = operator.gt # greater than (>)
- set_idx = 0
- elif current_player == Player.MIN:
- value = infinity
- best = min
- compare = operator.lt # less than (<)
- set_idx = 1
- elif current_player == Player.CHANCE:
- raise ValueError("CHANCE is not valid for regular minimax.")
- else:
- raise ValueError(f"Unexpected player type {current_player!r}")
-
- best_action: Action | None = None
- for action in cls.actions(state):
- result = cls.alphabeta(cls.result(state, action), next_down, a, b)
- new_value = best(value, result.value)
-
- if new_value != value:
- best_action = action
- value = new_value
-
- if compare(new_value, (a, b)[set_idx ^ 1]):
- # print("cutoff")
- break # cutoff
-
- alpha_beta_value = (a, b)[set_idx]
- new_alpha_beta_value = best(alpha_beta_value, value)
-
- if new_alpha_beta_value != alpha_beta_value:
- # Set new best
- alpha_beta_list = [a, b]
- alpha_beta_list[set_idx] = new_alpha_beta_value
- a, b = alpha_beta_list
- return MinimaxResult(value, best_action)
-
-
-class Expectiminimax(ABC, Generic[State, Action]):
- """Base class for Expectiminimax AIs."""
-
- __slots__ = ()
-
LOWEST = -1
HIGHEST = 1
@classmethod
@abstractmethod
def value(cls, state: State) -> int | float:
- """Return the value of a given game state."""
+ """Return the value of a given game state.
+
+ Should be in range [cls.LOWEST, cls.HIGHEST].
+ """
@classmethod
@abstractmethod
@@ -218,7 +85,7 @@ def probability(cls, action: Action) -> float:
Should be in range [0.0, 1.0] for 0% and 100% chance respectively.
"""
- return 1.0
+ raise NotImplementedError()
@classmethod
def minimax(
diff --git a/src/azul/game.py b/src/azul/game.py
index 29512a5..4b9708f 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -63,6 +63,7 @@
from azul.network_shared import DEFAULT_PORT, find_ip
from azul.server import GameServer
from azul.sound import SoundData, play_sound as base_play_sound
+from azul.state import Tile
from azul.statemachine import AsyncState
from azul.tools import (
lerp_color,
@@ -133,8 +134,6 @@
)
-from azul.state import Tile
-
TILESIZE = 15
# Colors
diff --git a/src/azul/server.py b/src/azul/server.py
index 2fc6c76..3769b39 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -31,7 +31,7 @@
import traceback
from collections import deque
from functools import partial
-from typing import TYPE_CHECKING, NoReturn, cast
+from typing import TYPE_CHECKING, NoReturn
import trio
@@ -58,7 +58,7 @@
from azul.state import State
if TYPE_CHECKING:
- from collections.abc import Awaitable, Callable, Iterable
+ from collections.abc import Awaitable, Callable
class ServerClient(EncryptedNetworkEventComponent):
@@ -123,7 +123,7 @@ async def handle_game_over(self, event: Event[int]) -> None:
async def handle_initial_config(
self,
- event: Event[tuple[Pos, int]],
+ event: Event[tuple[None, int]],
) -> None:
"""Read initial config event and reraise as server[write]->initial_config."""
board_size, player_turn = event.data
@@ -251,7 +251,6 @@ class GameServer(network.Server):
"state",
)
- board_size = (8, 8)
max_clients = 4
def __init__(self, internal_singleplayer_mode: bool = False) -> None:
@@ -461,7 +460,7 @@ async def handle_server_start_new_game(self, event: Event[None]) -> None:
await self.raise_event(
Event(
"initial_config->network",
- (self.board_size, self.state.turn),
+ (None, self.state.turn),
),
)
@@ -535,29 +534,18 @@ async def send_spectator_join_packets(
)
with self.temporary_component(private_events_pocket):
with private_events_pocket.temporary_component(client):
- ## # Send create_piece events for all pieces
- ## async with trio.open_nursery() as nursery:
- ## for piece_pos, piece_type in self.state.get_pieces():
- ## nursery.start_soon(
- ## client.raise_event,
- ## Event(
- ## "create_piece->network",
- ## (piece_pos, piece_type),
- ## ),
- ## )
-
- await client.raise_event(
- Event(f"playing_as->network[{client.client_id}]", 255),
- )
-
# Raise initial config event with board size and initial turn.
await client.raise_event(
Event(
"initial_config->network",
- (self.state.size, self.state.turn),
+ (None, self.state.turn),
),
)
+ await client.raise_event(
+ Event(f"playing_as->network[{client.client_id}]", 255),
+ )
+
async def handler(self, stream: trio.SocketStream) -> None:
"""Accept clients. Called by network.Server.serve."""
if self.client_count == 0 and self.game_active():
@@ -607,141 +595,6 @@ async def handler(self, stream: trio.SocketStream) -> None:
self.client_count -= 1
# ServerClient's `with` block handles closing stream.
- async def handle_network_select_piece(
- self,
- event: Event[tuple[int, Pos]],
- ) -> None:
- """Handle piece event from client."""
- client_id, tile_pos = event.data
-
- player = self.client_players.get(client_id, 0xFF)
- if player == 2:
- player = int(self.state.turn)
-
- if player != self.state.turn:
- print(
- f"{player = } cannot select piece {tile_pos = } because it is not that player's turn",
- )
- return
-
- if not self.players_can_interact:
- print(
- f"{player = } cannot select piece {tile_pos = } because players_can_interact is False",
- )
- return
- if not self.state.can_player_select_piece(player, tile_pos):
- print(f"{player = } cannot select piece {tile_pos = }")
- await self.player_select_piece(player, None)
- return
- if tile_pos == self.player_selections.get(player):
- # print(f"{player = } toggle select -> No select")
- await self.player_select_piece(player, None)
- return
-
- await self.player_select_piece(player, tile_pos)
-
- async def player_select_piece(
- self,
- player: int,
- piece_pos: Pos | None,
- ) -> None:
- """Update glowing tiles from new selected piece."""
- ignore: set[Pos] = set()
-
- if piece_pos is not None:
- # Calculate actions if required
- new_action_set = self.state.calculate_actions(piece_pos)
- ignore = new_action_set.ends
-
- ignored: set[Pos] = set()
-
- # Remove outlined tiles from previous selection if existed
- if prev_selection := self.player_selections.get(player):
- action_set = self.state.calculate_actions(prev_selection)
- ignored = action_set.ends & ignore
- remove = action_set.ends - ignore
- async with trio.open_nursery() as nursery:
- for tile_position in remove:
- nursery.start_soon(
- self.raise_event,
- Event("delete_tile->network", tile_position),
- )
- if piece_pos != prev_selection:
- nursery.start_soon(
- self.raise_event,
- Event(
- "select_piece->network",
- (prev_selection, False),
- ),
- )
-
- if piece_pos is None:
- if prev_selection:
- del self.player_selections[player]
- return
-
- self.player_selections[player] = piece_pos
-
- # For each end point
- async with trio.open_nursery() as nursery:
- for tile_position in new_action_set.ends - ignored:
- nursery.start_soon(
- self.raise_event,
- Event("create_tile->network", tile_position),
- )
- # Sent select piece as well
- nursery.start_soon(
- self.raise_event,
- Event(
- "select_piece->network",
- (self.player_selections[player], True),
- ),
- )
-
- async def handle_move_animation(self, from_pos: Pos, to_pos: Pos) -> None:
- """Handle move animation."""
- await self.raise_event(
- Event("move_piece_animation->network", (from_pos, to_pos)),
- )
-
- async def handle_jump_animation(self, jumped_pos: Pos) -> None:
- """Handle jump animation."""
- await self.raise_event(
- Event("delete_piece_animation->network", jumped_pos),
- )
-
- async def handle_king_animation(
- self,
- kinged_pos: Pos,
- piece_type: int,
- ) -> None:
- """Handle jump animation."""
- await self.raise_event(
- Event("update_piece_animation->network", (kinged_pos, piece_type)),
- )
-
- async def handle_action_animations(
- self,
- actions: deque[tuple[str, Iterable[Pos | int]]],
- ) -> None:
- """Handle action animations."""
- while actions:
- name, params = actions.popleft()
- if name == "move":
- await self.handle_move_animation(
- *cast("Iterable[Pos]", params),
- )
- elif name == "jump":
- await self.handle_jump_animation(
- *cast("Iterable[Pos]", params),
- )
- elif name == "king":
- await self.handle_king_animation(
- *cast("tuple[Pos, int]", params),
- )
- else:
- raise NotImplementedError(f"Animation for action {name}")
-
def __del__(self) -> None:
"""Debug print."""
print(f"del {self.__class__.__name__}")
diff --git a/src/azul/state.py b/src/azul/state.py
index 05236ff..5c836dc 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -36,7 +36,7 @@
TypeVar,
)
-from numpy import array, int8
+from numpy import full, int8
if TYPE_CHECKING:
from collections.abc import Generator
@@ -192,10 +192,7 @@ class PlayerData(NamedTuple):
@classmethod
def new(cls, varient_play: bool = False) -> Self:
"""Return new player data instance."""
- wall = array(
- [Tile.blank for _ in range(5 * 5)],
- int8,
- ).reshape((5, 5))
+ wall = full((5, 5), Tile.blank, int8)
if not varient_play:
for y in range(5):
@@ -376,7 +373,7 @@ def get_score_from_wall_placement(
count += self.get_vertically_linked_wall_count(x, y, wall)
return count
- def perform_floor_line_scoring(self) -> int:
+ def get_floor_line_scoring(self) -> int:
"""Return score increment value from floor line."""
total_count = self.floor.total()
assert total_count <= FLOOR_LINE_COUNT
@@ -412,7 +409,7 @@ def perform_auto_wall_tiling(self) -> tuple[Self, Counter[int], bool]:
PatternLine.blank(),
)
- score += self.perform_floor_line_scoring()
+ score += self.get_floor_line_scoring()
if score < 0:
score = 0
@@ -722,7 +719,7 @@ def _factory_offer_maybe_next_turn(self) -> Self:
)
if current_phase == Phase.wall_tiling:
if self.varient_play:
- raise NotImplementedError()
+ return new_state.start_manual_wall_tiling()
return new_state.apply_auto_wall_tiling()
return new_state
@@ -1025,12 +1022,17 @@ def preform_action(
return new
raise NotImplementedError()
+ def start_manual_wall_tiling(self) -> Self:
+ """Return new state after starting manual wall tiling."""
+ raise NotImplementedError()
+ return self
+
def run() -> None:
"""Run program."""
from market_api import pretty_print_response as pprint
- random.seed(0)
+ random.seed(2)
state = State.new_game(2)
ticks = 0
try:
From 1b5ce40806efe41f26e3768c5c5376abe84b7e67 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Mon, 25 Nov 2024 01:43:00 -0600
Subject: [PATCH 04/67] Switch to use my `libcomponent` module
---
LICENSE-THIRD-PARTY | 392 ------------------
pyproject.toml | 2 +-
src/azul/base_io.py | 701 --------------------------------
src/azul/buffer.py | 101 -----
src/azul/client.py | 64 +--
src/azul/component.py | 485 ----------------------
src/azul/encrypted_event.py | 130 ------
src/azul/encryption.py | 140 -------
src/azul/game.py | 154 +++----
src/azul/keyboard.py | 2 +-
src/azul/mr_floppy_test.py | 2 +-
src/azul/network.py | 512 -----------------------
src/azul/network_shared.py | 31 --
src/azul/objects.py | 3 +-
src/azul/server.py | 86 +---
src/azul/sprite.py | 2 +-
src/azul/utils.py | 49 ---
test-requirements.in | 2 +-
tests/helpers.py | 175 --------
tests/protocol_helpers.py | 87 ----
tests/test_base_io.py | 746 ----------------------------------
tests/test_buffer.py | 97 -----
tests/test_component.py | 370 -----------------
tests/test_encrypted_event.py | 79 ----
tests/test_encryption.py | 108 -----
tests/test_network.py | 178 --------
tests/test_utils.py | 121 ------
27 files changed, 77 insertions(+), 4742 deletions(-)
delete mode 100644 src/azul/base_io.py
delete mode 100644 src/azul/buffer.py
delete mode 100644 src/azul/component.py
delete mode 100644 src/azul/encrypted_event.py
delete mode 100644 src/azul/encryption.py
delete mode 100644 src/azul/network.py
delete mode 100644 src/azul/utils.py
delete mode 100644 tests/helpers.py
delete mode 100644 tests/protocol_helpers.py
delete mode 100644 tests/test_base_io.py
delete mode 100644 tests/test_buffer.py
delete mode 100644 tests/test_component.py
delete mode 100644 tests/test_encrypted_event.py
delete mode 100644 tests/test_encryption.py
delete mode 100644 tests/test_network.py
delete mode 100644 tests/test_utils.py
diff --git a/LICENSE-THIRD-PARTY b/LICENSE-THIRD-PARTY
index c7343f4..e1a17ae 100644
--- a/LICENSE-THIRD-PARTY
+++ b/LICENSE-THIRD-PARTY
@@ -1,395 +1,3 @@
----------------------------------------------------------------------------------------------------
- GNU LESSER GENERAL PUBLIC LICENSE
-Applies to:
- - Copyright (c) 2023, ItsDrike
- All rights reserved.
- - src/azul/base_io.py: Entire file
- - src/azul/buffer.py: Entire file
- - src/azul/utils.py: Entire file
- - src/azul/encryption.py: Almost entire file (see details below)
- - src/azul/encrypted_event.py: Almost entire file (see details below)
- - tests/helpers.py: Entire file
- - tests/protocol_helpers.py: Entire file
- - tests/test_base_io.py: Entire file
- - tests/test_buffer.py: Entire file
- - tests/test_encryption.py: Entire file
- - tests/test_utils.py: Entire file
----------------------------------------------------------------------------------------------------
- GNU LESSER GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-
- This version of the GNU Lesser General Public License incorporates
-the terms and conditions of version 3 of the GNU General Public
-License, supplemented by the additional permissions listed below.
-
- 0. Additional Definitions.
-
- As used herein, "this License" refers to version 3 of the GNU Lesser
-General Public License, and the "GNU GPL" refers to version 3 of the GNU
-General Public License.
-
- "The Library" refers to a covered work governed by this License,
-other than an Application or a Combined Work as defined below.
-
- An "Application" is any work that makes use of an interface provided
-by the Library, but which is not otherwise based on the Library.
-Defining a subclass of a class defined by the Library is deemed a mode
-of using an interface provided by the Library.
-
- A "Combined Work" is a work produced by combining or linking an
-Application with the Library. The particular version of the Library
-with which the Combined Work was made is also called the "Linked
-Version".
-
- The "Minimal Corresponding Source" for a Combined Work means the
-Corresponding Source for the Combined Work, excluding any source code
-for portions of the Combined Work that, considered in isolation, are
-based on the Application, and not on the Linked Version.
-
- The "Corresponding Application Code" for a Combined Work means the
-object code and/or source code for the Application, including any data
-and utility programs needed for reproducing the Combined Work from the
-Application, but excluding the System Libraries of the Combined Work.
-
- 1. Exception to Section 3 of the GNU GPL.
-
- You may convey a covered work under sections 3 and 4 of this License
-without being bound by section 3 of the GNU GPL.
-
- 2. Conveying Modified Versions.
-
- If you modify a copy of the Library, and, in your modifications, a
-facility refers to a function or data to be supplied by an Application
-that uses the facility (other than as an argument passed when the
-facility is invoked), then you may convey a copy of the modified
-version:
-
- a) under this License, provided that you make a good faith effort to
- ensure that, in the event an Application does not supply the
- function or data, the facility still operates, and performs
- whatever part of its purpose remains meaningful, or
-
- b) under the GNU GPL, with none of the additional permissions of
- this License applicable to that copy.
-
- 3. Object Code Incorporating Material from Library Header Files.
-
- The object code form of an Application may incorporate material from
-a header file that is part of the Library. You may convey such object
-code under terms of your choice, provided that, if the incorporated
-material is not limited to numerical parameters, data structure
-layouts and accessors, or small macros, inline functions and templates
-(ten or fewer lines in length), you do both of the following:
-
- a) Give prominent notice with each copy of the object code that the
- Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the object code with a copy of the GNU GPL and this license
- document.
-
- 4. Combined Works.
-
- You may convey a Combined Work under terms of your choice that,
-taken together, effectively do not restrict modification of the
-portions of the Library contained in the Combined Work and reverse
-engineering for debugging such modifications, if you also do each of
-the following:
-
- a) Give prominent notice with each copy of the Combined Work that
- the Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the Combined Work with a copy of the GNU GPL and this license
- document.
-
- c) For a Combined Work that displays copyright notices during
- execution, include the copyright notice for the Library among
- these notices, as well as a reference directing the user to the
- copies of the GNU GPL and this license document.
-
- d) Do one of the following:
-
- 0) Convey the Minimal Corresponding Source under the terms of this
- License, and the Corresponding Application Code in a form
- suitable for, and under terms that permit, the user to
- recombine or relink the Application with a modified version of
- the Linked Version to produce a modified Combined Work, in the
- manner specified by section 6 of the GNU GPL for conveying
- Corresponding Source.
-
- 1) Use a suitable shared library mechanism for linking with the
- Library. A suitable mechanism is one that (a) uses at run time
- a copy of the Library already present on the user's computer
- system, and (b) will operate properly with a modified version
- of the Library that is interface-compatible with the Linked
- Version.
-
- e) Provide Installation Information, but only if you would otherwise
- be required to provide such information under section 6 of the
- GNU GPL, and only to the extent that such information is
- necessary to install and execute a modified version of the
- Combined Work produced by recombining or relinking the
- Application with a modified version of the Linked Version. (If
- you use option 4d0, the Installation Information must accompany
- the Minimal Corresponding Source and Corresponding Application
- Code. If you use option 4d1, you must provide the Installation
- Information in the manner specified by section 6 of the GNU GPL
- for conveying Corresponding Source.)
-
- 5. Combined Libraries.
-
- You may place library facilities that are a work based on the
-Library side by side in a single library together with other library
-facilities that are not Applications and are not covered by this
-License, and convey such a combined library under terms of your
-choice, if you do both of the following:
-
- a) Accompany the combined library with a copy of the same work based
- on the Library, uncombined with any other library facilities,
- conveyed under the terms of this License.
-
- b) Give prominent notice with the combined library that part of it
- is a work based on the Library, and explaining where to find the
- accompanying uncombined form of the same work.
-
- 6. Revised Versions of the GNU Lesser General Public License.
-
- The Free Software Foundation may publish revised and/or new versions
-of the GNU Lesser General Public License from time to time. Such new
-versions will be similar in spirit to the present version, but may
-differ in detail to address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Library as you received it specifies that a certain numbered version
-of the GNU Lesser General Public License "or any later version"
-applies to it, you have the option of following the terms and
-conditions either of that published version or of any later version
-published by the Free Software Foundation. If the Library as you
-received it does not specify a version number of the GNU Lesser
-General Public License, you may choose any version of the GNU Lesser
-General Public License ever published by the Free Software Foundation.
-
- If the Library as you received it specifies that a proxy can decide
-whether future versions of the GNU Lesser General Public License shall
-apply, that proxy's public statement of acceptance of any version is
-permanent authorization for you to choose that version for the
-Library.
----------------------------------------------------------------------------------------------------
- Apache License version 2.0
-Applies to:
- - Copyright (c) 2012 Ammar Askar
- All rights reserved.
- - src/azul/encryption.py: encrypt_token_and_secret, generate_shared_secret functions
- - src/azul/encrypted_event.py: read, write functions
----------------------------------------------------------------------------------------------------
-
-
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
---------------------------------------------------------------------------------------------------
CREATIVE COMMONS 0
Applies to:
diff --git a/pyproject.toml b/pyproject.toml
index 1344d9c..b8a5cce 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,11 +36,11 @@ keywords = [
"ai", "multi-player", "azul", "ai-support", "networked-game"
]
dependencies = [
+ "libcomponent~=0.0.0",
"pygame~=2.6.0",
"typing_extensions>=4.12.2",
"mypy_extensions>=1.0.0",
"trio~=0.27.0",
- "cryptography>=43.0.0",
"exceptiongroup; python_version < '3.11'",
"numpy~=2.1.3",
]
diff --git a/src/azul/base_io.py b/src/azul/base_io.py
deleted file mode 100644
index f85b31f..0000000
--- a/src/azul/base_io.py
+++ /dev/null
@@ -1,701 +0,0 @@
-"""Base IO classes."""
-
-# This is the base_io module from https://github.com/py-mine/mcproto v0.3.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-import struct
-from abc import ABC, abstractmethod
-from enum import Enum
-from itertools import count
-from typing import TYPE_CHECKING, Literal, TypeAlias, TypeVar, overload
-
-from .utils import from_twos_complement, to_twos_complement
-
-if TYPE_CHECKING:
- from collections.abc import Awaitable, Callable
-
-__all__ = [
- "FLOAT_FORMATS_TYPE",
- "INT_FORMATS_TYPE",
- "BaseAsyncReader",
- "BaseAsyncWriter",
- "BaseSyncReader",
- "BaseSyncWriter",
- "StructFormat",
-]
-
-T = TypeVar("T")
-R = TypeVar("R")
-
-
-# region: Format types
-
-
-class StructFormat(str, Enum):
- """All possible write/read struct types.
-
- .. seealso:
- :module:`struct` module documentation.
- """
-
- BOOL = "?"
- CHAR = "c"
- BYTE = "b"
- UBYTE = "B"
- SHORT = "h"
- USHORT = "H"
- INT = "i"
- UINT = "I"
- LONG = "l"
- ULONG = "L"
- FLOAT = "f"
- DOUBLE = "d"
- HALFFLOAT = "e"
- LONGLONG = "q"
- ULONGLONG = "Q"
-
-
-INT_FORMATS_TYPE: TypeAlias = Literal[
- StructFormat.BYTE,
- StructFormat.UBYTE,
- StructFormat.SHORT,
- StructFormat.USHORT,
- StructFormat.INT,
- StructFormat.UINT,
- StructFormat.LONG,
- StructFormat.ULONG,
- StructFormat.LONGLONG,
- StructFormat.ULONGLONG,
-]
-
-FLOAT_FORMATS_TYPE: TypeAlias = Literal[
- StructFormat.FLOAT,
- StructFormat.DOUBLE,
- StructFormat.HALFFLOAT,
-]
-
-# endregion
-
-# region: Writer classes
-
-
-class BaseAsyncWriter(ABC):
- """Base class holding asynchronous write buffer/connection interactions."""
-
- __slots__ = ()
-
- @abstractmethod
- async def write(self, data: bytes, /) -> None:
- """Underlying write method, sending/storing the data.
-
- All the writer functions will eventually call this method.
- """
-
- @overload
- async def write_value(
- self,
- fmt: INT_FORMATS_TYPE,
- value: int,
- /,
- ) -> None: ...
-
- @overload
- async def write_value(
- self,
- fmt: FLOAT_FORMATS_TYPE,
- value: float,
- /,
- ) -> None: ...
-
- @overload
- async def write_value(
- self,
- fmt: Literal[StructFormat.BOOL],
- value: bool,
- /,
- ) -> None: ...
-
- @overload
- async def write_value(
- self,
- fmt: Literal[StructFormat.CHAR],
- value: str,
- /,
- ) -> None: ...
-
- async def write_value(self, fmt: StructFormat, value: object, /) -> None:
- """Write a given ``value`` as given struct format (``fmt``) in big-endian mode."""
- await self.write(struct.pack(">" + fmt.value, value))
-
- async def _write_varuint(
- self,
- value: int,
- /,
- *,
- max_bits: int | None = None,
- ) -> None:
- """Write an arbitrarily big unsigned integer in a variable length format.
-
- This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes.
-
- Writing will be limited up to integer values of ``max_bits`` bits, and trying to write bigger values will rase
- a :exc:`ValueError`. Note that setting ``max_bits`` to for example 32 bits doesn't mean that at most 4 bytes
- will be sent, in this case it would actually take at most 5 bytes, due to the variable encoding overhead.
-
- Varints send bytes where 7 least significant bits are value bits, and the most significant bit is continuation
- flag bit. If this continuation bit is set (1), it indicates that there will be another varint byte sent after
- this one. The least significant group is written first, followed by each of the more significant groups, making
- varints little-endian, however in groups of 7 bits, not 8.
- """
- value_max = (
- (1 << (max_bits)) - 1 if max_bits is not None else float("inf")
- )
- if value < 0 or value > value_max:
- raise ValueError(
- f"Tried to write varint outside of the range of {max_bits}-bit int.",
- )
-
- remaining = value
- while True:
- if remaining & -0x80 == 0: # final byte (~0x7F)
- await self.write_value(StructFormat.UBYTE, remaining)
- return
- # Write only 7 least significant bits with the first bit being 1, marking there will be another byte
- await self.write_value(StructFormat.UBYTE, remaining & 0x7F | 0x80)
- # Subtract the value we've already sent (7 least significant bits)
- remaining >>= 7
-
- async def write_varint(self, value: int, /) -> None:
- """Write a 32-bit signed integer in a variable length format.
-
- For more information about variable length format check :meth:`._write_varuint`.
-
- Raises ValueError if value is outside of the range of a 32-bit signed integer.
- """
- val = to_twos_complement(value, bits=32)
- await self._write_varuint(val, max_bits=32)
-
- async def write_varlong(self, value: int, /) -> None:
- """Write a 64-bit signed integer in a variable length format.
-
- For more information about variable length format check :meth:`._write_varuint`.
-
- Raises ValueError if value is outside of the range of a 64-bit signed integer.
- """
- val = to_twos_complement(value, bits=64)
- await self._write_varuint(val, max_bits=64)
-
- async def write_bytearray(self, data: bytes, /) -> None:
- """Write an arbitrary sequence of bytes, prefixed with a varint of it's size.
-
- Raises ValueError if length is is outside of the range of a 32-bit signed integer.
- """
- await self.write_varint(len(data))
- await self.write(data)
-
- async def write_ascii(self, value: str, /) -> None:
- """Write ISO-8859-1 encoded string, with NULL (0x00) at the end to indicate string end."""
- data = bytearray(value, "ISO-8859-1")
- await self.write(data)
- await self.write(bytearray.fromhex("00"))
-
- async def write_utf(self, value: str, /) -> None:
- """Write a UTF-8 encoded string, prefixed with a varint of it's size (in bytes).
-
- The maximum amount of UTF-8 characters is limited to 32767.
-
- Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the
- worst case of 4 bytes per every character, at most 131068 data bytes will be written + 3 additional bytes from
- the varint encoding overhead.
-
- :raises ValueError:
- If the given string ``value`` has more characters than the allowed maximum (32767).
- """
- if len(value) > 32767:
- raise ValueError(
- "Maximum character limit for writing strings is 32767 characters.",
- )
-
- data = bytearray(value, "utf-8")
- await self.write_varint(len(data))
- await self.write(data)
-
- async def write_optional(
- self,
- value: T | None,
- /,
- writer: Callable[[T], Awaitable[R]],
- ) -> R | None:
- """Write a bool showing if a ``value`` is present, if so, also writes this value with ``writer`` function.
-
- * When ``value`` is ``None``, a bool of ``False`` will be written, and ``None`` is returned.
- * When ``value`` is not ``None``, a bool of ``True`` is written, after which the ``writer`` function is called,
- and the return value is forwarded.
- """
- if value is None:
- await self.write_value(StructFormat.BOOL, False)
- return None
-
- await self.write_value(StructFormat.BOOL, True)
- return await writer(value)
-
-
-class BaseSyncWriter(ABC):
- """Base class holding synchronous write buffer/connection interactions."""
-
- __slots__ = ()
-
- @abstractmethod
- def write(self, data: bytes, /) -> None:
- """Write data."""
- ...
-
- @overload
- def write_value(self, fmt: INT_FORMATS_TYPE, value: int, /) -> None: ...
-
- @overload
- def write_value(
- self,
- fmt: FLOAT_FORMATS_TYPE,
- value: float,
- /,
- ) -> None: ...
-
- @overload
- def write_value(
- self,
- fmt: Literal[StructFormat.BOOL],
- value: bool,
- /,
- ) -> None: ...
-
- @overload
- def write_value(
- self,
- fmt: Literal[StructFormat.CHAR],
- value: str,
- /,
- ) -> None: ...
-
- def write_value(self, fmt: StructFormat, value: object, /) -> None:
- """Write a given ``value`` as given struct format (``fmt``) in big-endian mode."""
- self.write(struct.pack(">" + fmt.value, value))
-
- def _write_varuint(
- self,
- value: int,
- /,
- *,
- max_bits: int | None = None,
- ) -> None:
- """Write an arbitrarily big unsigned integer in a variable length format.
-
- This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes.
-
- Writing will be limited up to integer values of ``max_bits`` bits, and trying to write bigger values will rase
- a :exc:`ValueError`. Note that setting ``max_bits`` to for example 32 bits doesn't mean that at most 4 bytes
- will be sent, in this case it would actually take at most 5 bytes, due to the variable encoding overhead.
-
- Varints send bytes where 7 least significant bits are value bits, and the most significant bit is continuation
- flag bit. If this continuation bit is set (1), it indicates that there will be another varint byte sent after
- this one. The least significant group is written first, followed by each of the more significant groups, making
- varints little-endian, however in groups of 7 bits, not 8.
- """
- value_max = (
- (1 << (max_bits)) - 1 if max_bits is not None else float("inf")
- )
- if value < 0 or value > value_max:
- raise ValueError(
- f"Tried to write varint outside of the range of {max_bits}-bit int.",
- )
-
- remaining = value
- while True:
- if remaining & ~0x7F == 0: # final byte
- self.write_value(StructFormat.UBYTE, remaining)
- return
- # Write only 7 least significant bits with the first bit being 1, marking there will be another byte
- self.write_value(StructFormat.UBYTE, remaining & 0x7F | 0x80)
- # Subtract the value we've already sent (7 least significant bits)
- remaining >>= 7
-
- def write_varint(self, value: int, /) -> None:
- """Write a 32-bit signed integer in a variable length format.
-
- For more information about variable length format check :meth:`._write_varuint`.
-
- Raises ValueError if length is is outside of the range of a 32-bit signed integer.
- """
- val = to_twos_complement(value, bits=32)
- self._write_varuint(val, max_bits=32)
-
- def write_varlong(self, value: int, /) -> None:
- """Write a 64-bit signed integer in a variable length format.
-
- For more information about variable length format check :meth:`._write_varuint` docstring.
-
- Raises ValueError if length is is outside of the range of a 64-bit signed integer.
- """
- val = to_twos_complement(value, bits=64)
- self._write_varuint(val, max_bits=64)
-
- def write_bytearray(self, data: bytes, /) -> None:
- """Write an arbitrary sequence of bytes, prefixed with a varint of it's size.
-
- Raises ValueError if length is is outside of the range of a 32-bit signed integer.
- """
- self.write_varint(len(data))
- self.write(data)
-
- def write_ascii(self, value: str, /) -> None:
- """Write ISO-8859-1 encoded string, with NULL (0x00) at the end to indicate string end."""
- data = bytearray(value, "ISO-8859-1")
- self.write(data)
- self.write(bytearray.fromhex("00"))
-
- def write_utf(self, value: str, /) -> None:
- """Write a UTF-8 encoded string, prefixed with a varint of it's size (in bytes).
-
- The maximum amount of UTF-8 characters is limited to 32767.
-
- Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the
- worst case of 4 bytes per every character, at most 131068 data bytes will be written + 3 additional bytes from
- the varint encoding overhead.
-
- :raises ValueError:
- If the given string ``value`` has more characters than the allowed maximum (32767).
- """
- if len(value) > 32767:
- raise ValueError(
- "Maximum character limit for writing strings is 32767 characters.",
- )
-
- data = bytearray(value, "utf-8")
- self.write_varint(len(data))
- self.write(data)
-
- def write_optional(
- self,
- value: T | None,
- /,
- writer: Callable[[T], R],
- ) -> R | None:
- """Write a bool showing if a ``value`` is present, if so, also writes this value with ``writer`` function.
-
- * When ``value`` is ``None``, a bool of ``False`` will be written, and ``None`` is returned.
- * When ``value`` is not ``None``, a bool of ``True`` is written, after which the ``writer`` function is called,
- and the return value is forwarded.
- """
- if value is None:
- self.write_value(StructFormat.BOOL, False)
- return None
-
- self.write_value(StructFormat.BOOL, True)
- return writer(value)
-
-
-# endregion
-# region: Reader classes
-
-
-class BaseAsyncReader(ABC):
- """Base class holding asynchronous read buffer/connection interactions."""
-
- __slots__ = ()
-
- @abstractmethod
- async def read(self, length: int, /) -> bytearray:
- """Underlying read method, obtaining the raw data.
-
- All of the reader functions will eventually call this method.
- """
-
- @overload
- async def read_value(self, fmt: INT_FORMATS_TYPE, /) -> int: ...
-
- @overload
- async def read_value(self, fmt: FLOAT_FORMATS_TYPE, /) -> float: ...
-
- @overload
- async def read_value(self, fmt: Literal[StructFormat.BOOL], /) -> bool: ...
-
- @overload
- async def read_value(self, fmt: Literal[StructFormat.CHAR], /) -> str: ...
-
- async def read_value(self, fmt: StructFormat, /) -> object:
- """Read a value as given struct format (``fmt``) in big-endian mode.
-
- The amount of bytes to read will be determined based on the struct format automatically.
- """
- length = struct.calcsize(fmt.value)
- data = await self.read(length)
- unpacked = struct.unpack(">" + fmt.value, data)
- return unpacked[0]
-
- async def _read_varuint(self, *, max_bits: int | None = None) -> int:
- """Read an arbitrarily big unsigned integer in a variable length format.
-
- This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes.
-
- Reading will be limited up to integer values of ``max_bits`` bits, and trying to read bigger values will rase
- an :exc:`OSError`. Note that setting ``max_bits`` to for example 32 bits doesn't mean that at most 4 bytes
- will be read, in this case we would actually read at most 5 bytes, due to the variable encoding overhead.
-
- Varints send bytes where 7 least significant bits are value bits, and the most significant bit is continuation
- flag bit. If this continuation bit is set (1), it indicates that there will be another varint byte sent after
- this one. The least significant group is written first, followed by each of the more significant groups, making
- varints little-endian, however in groups of 7 bits, not 8.
- """
- value_max = (
- (1 << (max_bits)) - 1 if max_bits is not None else float("inf")
- )
-
- result = 0
- for i in count():
- byte = await self.read_value(StructFormat.UBYTE)
- # Read 7 least significant value bits in this byte, and shift them appropriately to be in the right place
- # then simply add them (OR) as additional 7 most significant bits in our result
- result |= (byte & 0x7F) << (7 * i)
-
- # Ensure that we stop reading and raise an error if the size gets over the maximum
- # (if the current amount of bits is higher than allowed size in bits)
- if result > value_max:
- raise OSError(
- f"Received varint was outside the range of {max_bits}-bit int.",
- )
-
- # If the most significant bit is 0, we should stop reading
- if not byte & 0x80:
- break
-
- return result
-
- async def read_varint(self) -> int:
- """Read a 32-bit signed integer in a variable length format.
-
- For more information about variable length format check :meth:`._read_varuint`.
- """
- unsigned_num = await self._read_varuint(max_bits=32)
- return from_twos_complement(unsigned_num, bits=32)
-
- async def read_varlong(self) -> int:
- """Read a 64-bit signed integer in a variable length format.
-
- For more information about variable length format check :meth:`._read_varuint`.
- """
- unsigned_num = await self._read_varuint(max_bits=64)
- return from_twos_complement(unsigned_num, bits=64)
-
- async def read_bytearray(self, /) -> bytearray:
- """Read an arbitrary sequence of bytes, prefixed with a varint of it's size."""
- length = await self.read_varint()
- return await self.read(length)
-
- async def read_ascii(self) -> str:
- """Read ISO-8859-1 encoded string, until we encounter NULL (0x00) at the end indicating string end."""
- # Keep reading bytes until we find NULL
- result = bytearray()
- while len(result) == 0 or result[-1] != 0:
- byte = await self.read(1)
- result.extend(byte)
- return result[:-1].decode("ISO-8859-1")
-
- async def read_utf(self) -> str:
- """Read a UTF-8 encoded string, prefixed with a varint of it's size (in bytes).
-
- The maximum amount of UTF-8 characters is limited to 32767.
-
- Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the
- worst case of 4 bytes per every character, at most 131068 data bytes will be read + 3 additional bytes from
- the varint encoding overhead.
-
- :raises IOError:
- * If the prefix varint is bigger than the maximum (131068) bytes, the string will not be read at all,
- and :exc:`IOError` will be raised immediately.
- * If the received string has more than the maximum amount of characters (32767). Note that in this case,
- the string will still get read in it's entirety, since it fits into the maximum bytes limit (131068),
- which was simply read at once. This limitation is here only to replicate the behavior of minecraft's
- implementation.
- """
- length = await self.read_varint()
- if length > 131068:
- raise OSError(
- f"Maximum read limit for utf strings is 131068 bytes, got {length}.",
- )
-
- data = await self.read(length)
- chars = data.decode("utf-8")
-
- if len(chars) > 32767:
- raise OSError(
- f"Maximum read limit for utf strings is 32767 characters, got {len(chars)}.",
- )
-
- return chars
-
- async def read_optional(
- self,
- reader: Callable[[], Awaitable[R]],
- ) -> R | None:
- """Read a bool showing if a value is present, if so, also reads this value with ``reader`` function.
-
- * When ``False`` is read, the function will not read anything and ``None`` is returned.
- * When ``True`` is read, the ``reader`` function is called, and it's return value is forwarded.
- """
- if not await self.read_value(StructFormat.BOOL):
- return None
-
- return await reader()
-
-
-class BaseSyncReader(ABC):
- """Base class holding synchronous read buffer/connection interactions."""
-
- __slots__ = ()
-
- @abstractmethod
- def read(self, length: int, /) -> bytearray:
- """Read ``length`` bytes and return in a bytearray."""
- ...
-
- @overload
- def read_value(self, fmt: INT_FORMATS_TYPE, /) -> int: ...
-
- @overload
- def read_value(self, fmt: FLOAT_FORMATS_TYPE, /) -> float: ...
-
- @overload
- def read_value(self, fmt: Literal[StructFormat.BOOL], /) -> bool: ...
-
- @overload
- def read_value(self, fmt: Literal[StructFormat.CHAR], /) -> str: ...
-
- def read_value(self, fmt: StructFormat, /) -> object:
- """Read a value into given struct format in big-endian mode.
-
- The amount of bytes to read will be determined based on the struct format automatically.
- """
- length = struct.calcsize(fmt.value)
- data = self.read(length)
- unpacked = struct.unpack(">" + fmt.value, data)
- return unpacked[0]
-
- def _read_varuint(self, *, max_bits: int | None = None) -> int:
- """Read an arbitrarily big unsigned integer in a variable length format.
-
- This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes.
-
- Reading will be limited up to integer values of ``max_bits`` bits, and trying to read bigger values will rase
- an :exc:`IOError`. Note that setting ``max_bits`` to for example 32 bits doesn't mean that at most 4 bytes
- will be read, in this case we would actually read at most 5 bytes, due to the variable encoding overhead.
-
- Varints send bytes where 7 least significant bits are value bits, and the most significant bit is continuation
- flag bit. If this continuation bit is set (1), it indicates that there will be another varint byte sent after
- this one. The least significant group is written first, followed by each of the more significant groups, making
- varints little-endian, however in groups of 7 bits, not 8.
- """
- value_max = (
- (1 << (max_bits)) - 1 if max_bits is not None else float("inf")
- )
-
- result = 0
- for i in count():
- byte = self.read_value(StructFormat.UBYTE)
- # Read 7 least significant value bits in this byte, and shift them appropriately to be in the right place
- # then simply add them (OR) as additional 7 most significant bits in our result
- result |= (byte & 0x7F) << (7 * i)
-
- # Ensure that we stop reading and raise an error if the size gets over the maximum
- # (if the current amount of bits is higher than allowed size in bits)
- if result > value_max:
- raise OSError(
- f"Received varint was outside the range of {max_bits}-bit int.",
- )
-
- # If the most significant bit is 0, we should stop reading
- if not byte & 0x80:
- break
-
- return result
-
- def read_varint(self) -> int:
- """Read a 32-bit signed integer in a variable length format.
-
- For more information about variable length format check :meth:`._read_varuint`.
- """
- unsigned_num = self._read_varuint(max_bits=32)
- return from_twos_complement(unsigned_num, bits=32)
-
- def read_varlong(self) -> int:
- """Read a 64-bit signed integer in a variable length format.
-
- For more information about variable length format check :meth:`._read_varuint`.
- """
- unsigned_num = self._read_varuint(max_bits=64)
- return from_twos_complement(unsigned_num, bits=64)
-
- def read_bytearray(self) -> bytearray:
- """Read an arbitrary sequence of bytes, prefixed with a varint of it's size."""
- length = self.read_varint()
- return self.read(length)
-
- def read_ascii(self) -> str:
- """Read ISO-8859-1 encoded string, until we encounter NULL (0x00) at the end indicating string end."""
- # Keep reading bytes until we find NULL
- result = bytearray()
- while len(result) == 0 or result[-1] != 0:
- byte = self.read(1)
- result.extend(byte)
- return result[:-1].decode("ISO-8859-1")
-
- def read_utf(self) -> str:
- """Read a UTF-8 encoded string, prefixed with a varint of it's size (in bytes).
-
- The maximum amount of UTF-8 characters is limited to 32767.
-
- Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the
- worst case of 4 bytes per every character, at most 131068 data bytes will be read + 3 additional bytes from
- the varint encoding overhead.
-
- :raises IOError:
- * If the prefix varint is bigger than the maximum (131068) bytes, the string will not be read at all,
- and :exc:`IOError` will be raised immediately.
- * If the received string has more than the maximum amount of characters (32767). Note that in this case,
- the string will still get read in it's entirety, since it fits into the maximum bytes limit (131068),
- which was simply read at once. This limitation is here only to replicate the behavior of minecraft's
- implementation.
- """
- length = self.read_varint()
- if length > 131068:
- raise OSError(
- f"Maximum read limit for utf strings is 131068 bytes, got {length}.",
- )
-
- data = self.read(length)
- chars = data.decode("utf-8")
-
- if len(chars) > 32767:
- raise OSError(
- f"Maximum read limit for utf strings is 32767 characters, got {len(chars)}.",
- )
-
- return chars
-
- def read_optional(self, reader: Callable[[], R]) -> R | None:
- """Read a bool showing if a value is present, if so, also reads this value with ``reader`` function.
-
- * When ``False`` is read, the function will not read anything and ``None`` is returned.
- * When ``True`` is read, the ``reader`` function is called, and it's return value is forwarded.
- """
- if not self.read_value(StructFormat.BOOL):
- return None
-
- return reader()
-
-
-# endregion
diff --git a/src/azul/buffer.py b/src/azul/buffer.py
deleted file mode 100644
index 8207bc2..0000000
--- a/src/azul/buffer.py
+++ /dev/null
@@ -1,101 +0,0 @@
-"""Buffer module."""
-
-# This is the buffer module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-from typing import Any
-
-from .base_io import BaseSyncReader, BaseSyncWriter
-
-__all__ = ["Buffer"]
-
-
-class Buffer(BaseSyncWriter, BaseSyncReader, bytearray):
- """In-memory bytearray-like buffer supporting the common read/write operations."""
-
- __slots__ = ("pos",)
-
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- """Initialize starting at position zero."""
- super().__init__(*args, **kwargs)
- self.pos = 0
-
- def write(self, data: bytes) -> None:
- """Write/Store given ``data`` into the buffer."""
- self.extend(data)
-
- def read(self, length: int) -> bytearray:
- """Read data stored in the buffer.
-
- Reading data doesn't remove that data, rather that data is treated as already read, and
- next read will start from the first unread byte. If freeing the data is necessary, check
- the :meth:`.clear` function.
-
- :param length:
- Amount of bytes to be read.
-
- If the requested amount can't be read (buffer doesn't contain that much data/buffer
- doesn't contain any data), an :exc:`IOError` will be re-raised.
-
- If there were some data in the buffer, but it was less than requested, this remaining
- data will still be depleted and the partial data that was read will be a part of the
- error message in the :exc:`IOError`. This behavior is here to mimic reading from a real
- socket connection.
- """
- end = self.pos + length
-
- if end > len(self):
- data = self[self.pos : len(self)]
- bytes_read = len(self) - self.pos
- self.pos = len(self)
- raise OSError(
- "Requested to read more data than available."
- f" Read {bytes_read} bytes: {data}, out of {length} requested bytes.",
- )
-
- try:
- return self[self.pos : end]
- finally:
- self.pos = end
-
- def clear(self, only_already_read: bool = False) -> None:
- """Clear out the stored data and reset position.
-
- :param only_already_read:
- When set to ``True``, only the data that was already marked as read will be cleared,
- and the position will be reset (to start at the remaining data). This can be useful
- for avoiding needlessly storing large amounts of data in memory, if this data is no
- longer useful.
-
- Otherwise, if set to ``False``, all of the data is cleared, and the position is reset,
- essentially resulting in a blank buffer.
- """
- if only_already_read:
- del self[: self.pos]
- else:
- super().clear()
- self.pos = 0
-
- def reset(self) -> None:
- """Reset the position in the buffer.
-
- Since the buffer doesn't automatically clear the already read data, it is possible to simply
- reset the position and read the data it contains again.
- """
- self.pos = 0
-
- def flush(self) -> bytearray:
- """Read all of the remaining data in the buffer and clear it out."""
- data = self[self.pos : len(self)]
- self.clear()
- return data
-
- @property
- def remaining(self) -> int:
- """Get the amount of bytes that's still remaining in the buffer to be read."""
- return len(self) - self.pos
diff --git a/src/azul/client.py b/src/azul/client.py
index 697101c..d830f74 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -25,22 +25,16 @@
__version__ = "0.0.0"
import struct
-import time
import traceback
from typing import TYPE_CHECKING
import trio
+from libcomponent import network
+from libcomponent.base_io import StructFormat
+from libcomponent.buffer import Buffer
+from libcomponent.component import Event
+from libcomponent.network_utils import ClientNetworkEventComponent
-from azul import network
-from azul.base_io import StructFormat
-from azul.buffer import Buffer
-from azul.component import Event
-from azul.encrypted_event import EncryptedNetworkEventComponent
-from azul.encryption import (
- deserialize_public_key,
- encrypt_token_and_secret,
- generate_shared_secret,
-)
from azul.network_shared import (
ADVERTISEMENT_IP,
ADVERTISEMENT_PORT,
@@ -148,7 +142,7 @@ async def read_advertisements(
return response
-class GameClient(EncryptedNetworkEventComponent):
+class GameClient(ClientNetworkEventComponent):
"""Game Client Network Event Component.
This class handles connecting to the game server, transmitting events
@@ -325,18 +319,6 @@ async def handle_client_connect(
return
await self.raise_disconnect("Error connecting to server.")
- async def read_callback_ping(self, event: Event[bytearray]) -> None:
- """Read callback_ping event from server."""
- ns = int.from_bytes(event.data)
- now = int(time.time() * 1e9)
- difference = now - ns
-
- # print(f'{difference / 1e9 = } seconds')
-
- await self.raise_event(
- Event("callback_ping", difference),
- )
-
async def read_game_over(self, event: Event[bytearray]) -> None:
"""Read update_piece event from server."""
buffer = Buffer(event.data)
@@ -367,40 +349,6 @@ async def read_playing_as(self, event: Event[bytearray]) -> None:
Event("game_playing_as", playing_as),
)
- async def write_encryption_response(
- self,
- shared_secret: bytes,
- verify_token: bytes,
- ) -> None:
- """Write encryption response to server."""
- buffer = Buffer()
- buffer.write_bytearray(shared_secret)
- buffer.write_bytearray(verify_token)
-
- await self.write_event(Event("encryption_response->server", buffer))
-
- async def read_encryption_request(self, event: Event[bytearray]) -> None:
- """Read and handle encryption request from server."""
- buffer = Buffer(event.data)
-
- serialized_public_key = buffer.read_bytearray()
- verify_token = buffer.read_bytearray()
-
- public_key = deserialize_public_key(serialized_public_key)
-
- shared_secret = generate_shared_secret()
-
- encrypted_token, encrypted_secret = encrypt_token_and_secret(
- public_key,
- verify_token,
- shared_secret,
- )
-
- await self.write_encryption_response(encrypted_secret, encrypted_token)
-
- # Start encrypting all future data
- self.enable_encryption(shared_secret, verify_token)
-
async def handle_network_stop(self, event: Event[None]) -> None:
"""Send EOF if connected and close socket."""
if self.not_connected:
diff --git a/src/azul/component.py b/src/azul/component.py
deleted file mode 100644
index 487db3a..0000000
--- a/src/azul/component.py
+++ /dev/null
@@ -1,485 +0,0 @@
-"""Component system module - Components instead of chaotic class hierarchy mess."""
-
-# Programmed by CoolCat467
-
-# Copyright (C) 2023-2024 CoolCat467
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-from __future__ import annotations
-
-__title__ = "Component"
-__author__ = "CoolCat467"
-__license__ = "GNU General Public License Version 3"
-__version__ = "0.0.0"
-
-from contextlib import contextmanager
-from typing import TYPE_CHECKING, Any, Generic, TypeVar
-from weakref import ref
-
-import trio
-
-if TYPE_CHECKING:
- from collections.abc import Awaitable, Callable, Generator, Iterable
-
- from mypy_extensions import u8
-
-T = TypeVar("T")
-
-
-class Event(Generic[T]):
- """Event with name, data, and re-raise levels."""
-
- __slots__ = ("data", "level", "name")
-
- def __init__(
- self,
- name: str,
- data: T,
- levels: u8 = 0,
- ) -> None:
- """Initialize event."""
- self.name = name
- self.data = data
- self.level = levels
-
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"{self.__class__.__name__}({self.name!r}, {self.data!r}, {self.level!r})"
-
- def pop_level(self) -> bool:
- """Travel up one level and return True if event should continue or not."""
- continue_level = self.level > 0
- self.level = max(0, self.level - 1)
- return continue_level
-
-
-class Component:
- """Component base class."""
-
- __slots__ = ("__manager", "name")
-
- def __init__(self, name: object) -> None:
- """Initialise with name."""
- self.name = name
- self.__manager: ref[ComponentManager] | None = None
-
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"{self.__class__.__name__}({self.name!r})"
-
- @property
- def manager(self) -> ComponentManager:
- """ComponentManager if bound to one, otherwise raise AttributeError."""
- if self.__manager is not None:
- manager = self.__manager()
- if manager is not None:
- return manager
- raise AttributeError(f"No component manager bound for {self.name}")
-
- def _unbind(self) -> None:
- """If you use this you are evil. This is only for ComponentManagers!."""
- self.__manager = None
-
- @property
- def manager_exists(self) -> bool:
- """Return if manager is bound or not."""
- return self.__manager is not None and self.__manager() is not None
-
- def register_handler(
- self,
- event_name: str,
- handler_coro: Callable[[Event[Any]], Awaitable[Any]],
- ) -> None:
- """Register handler with bound component manager.
-
- Raises AttributeError if this component is not bound.
- """
- self.manager.register_component_handler(
- event_name,
- handler_coro,
- self.name,
- )
-
- def register_handlers(
- self,
- handlers: dict[str, Callable[[Event[Any]], Awaitable[Any]]],
- ) -> None:
- """Register multiple handler Coroutines.
-
- Raises AttributeError if this component is not bound.
- """
- for name, coro in handlers.items():
- self.register_handler(name, coro)
-
- def bind_handlers(self) -> None:
- """Add handlers in subclass."""
-
- def bind(self, manager: ComponentManager) -> None:
- """Bind self to manager.
-
- Raises RuntimeError if component is already bound to a manager.
- """
- if self.manager_exists:
- raise RuntimeError(
- f"{self.name} component is already bound to {self.manager}",
- )
- self.__manager = ref(manager)
- self.bind_handlers()
-
- def has_handler(self, event_name: str) -> bool:
- """Return if manager has event handlers registered for a given event.
-
- Raises AttributeError if this component is not bound.
- """
- return self.manager.has_handler(event_name)
-
- def unregister_handler(
- self,
- event_name: str,
- handler_coro: Callable[[Event[Any]], Awaitable[None]],
- ) -> None:
- """Unregister a handler function for event_name.
-
- Raises ValueError if no component with given name is registered.
- """
- return self.manager.unregister_handler(
- event_name,
- handler_coro,
- self.name,
- )
-
- def unregister_handler_type(self, event_name: str) -> bool:
- """Unregister all event handlers for a given event type."""
- return self.manager.unregister_handler_type(event_name)
-
- async def raise_event(self, event: Event[Any]) -> None:
- """Raise event for bound manager.
-
- Raises AttributeError if this component is not bound.
- """
- await self.manager.raise_event(event)
-
- def component_exists(self, component_name: str) -> bool:
- """Return if component exists in manager.
-
- Raises AttributeError if this component is not bound.
- """
- return self.manager.component_exists(component_name)
-
- def components_exist(self, component_names: Iterable[str]) -> bool:
- """Return if all component names given exist in manager.
-
- Raises AttributeError if this component is not bound.
- """
- return self.manager.components_exist(component_names)
-
- def get_component(self, component_name: str) -> Any:
- """Get Component from manager.
-
- Raises AttributeError if this component is not bound.
- """
- return self.manager.get_component(component_name)
-
- def get_components(
- self,
- component_names: Iterable[str],
- ) -> list[Component]:
- """Return Components from manager.
-
- Raises AttributeError if this component is not bound.
- """
- return self.manager.get_components(component_names)
-
-
-ComponentPassthrough = TypeVar("ComponentPassthrough", bound=Component)
-
-
-class ComponentManager(Component):
- """Component manager class."""
-
- __slots__ = ("__components", "__event_handlers", "__weakref__")
-
- def __init__(self, name: object, own_name: object | None = None) -> None:
- """If own_name is set, add self to list of components as specified name."""
- super().__init__(name)
- self.__event_handlers: dict[
- str,
- set[tuple[Callable[[Event[Any]], Awaitable[Any]], object]],
- ] = {}
- self.__components: dict[object, Component] = {}
-
- if own_name is not None:
- self.__add_self_as_component(own_name)
- self.bind_handlers()
-
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"<{self.__class__.__name__} Components: {self.__components}>"
-
- def __add_self_as_component(self, name: object) -> None:
- """Add this manager as component to self without binding.
-
- Raises ValueError if a component with given name already exists.
- """
- if self.component_exists(name): # pragma: nocover
- raise ValueError(f'Component named "{name}" already exists!')
- self.__components[name] = self
-
- def register_handler(
- self,
- event_name: str,
- handler_coro: Callable[[Event[Any]], Awaitable[None]],
- ) -> None:
- """Register handler_func as handler for event_name (self component)."""
- self.register_component_handler(event_name, handler_coro, self.name)
-
- def register_component_handler(
- self,
- event_name: str,
- handler_coro: Callable[[Event[Any]], Awaitable[None]],
- component_name: object,
- ) -> None:
- """Register handler_func as handler for event_name.
-
- Raises ValueError if no component with given name is registered.
- """
- if (
- component_name != self.name
- and component_name not in self.__components
- ):
- raise ValueError(
- f"Component named {component_name!r} is not registered!",
- )
- if event_name not in self.__event_handlers:
- self.__event_handlers[event_name] = set()
- self.__event_handlers[event_name].add((handler_coro, component_name))
-
- def unregister_handler(
- self,
- event_name: str,
- handler_coro: Callable[[Event[Any]], Awaitable[None]],
- component_name: object,
- ) -> None:
- """Unregister a handler function for event_name.
-
- Raises ValueError if no component with given name is registered.
- """
- if (
- component_name != self.name
- and component_name not in self.__components
- ):
- raise ValueError(
- f"Component named {component_name!r} is not registered!",
- )
-
- if event_name not in self.__event_handlers:
- return
-
- handler_tuple = (handler_coro, component_name)
- if handler_tuple in self.__event_handlers[event_name]:
- self.__event_handlers[event_name].remove(handler_tuple)
-
- # If the event_name no longer has any handlers, remove it
- if not self.__event_handlers[event_name]:
- del self.__event_handlers[event_name]
-
- def unregister_handler_type(
- self,
- event_name: str,
- ) -> None:
- """Unregister all event handlers for a given event type."""
- if event_name in self.__event_handlers:
- del self.__event_handlers[event_name]
-
- def has_handler(self, event_name: str) -> bool:
- """Return if there are event handlers registered for a given event."""
- return bool(self.__event_handlers.get(event_name))
-
- async def raise_event_in_nursery(
- self,
- event: Event[Any],
- nursery: trio.Nursery,
- ) -> None:
- """Raise event in a particular trio nursery.
-
- Could raise RuntimeError if given nursery is no longer open.
- """
- await trio.lowlevel.checkpoint()
-
- # Forward leveled events up; They'll come back to us soon enough.
- if self.manager_exists and event.pop_level():
- await super().raise_event(event)
- return
- # Make sure events not raised twice
- # if not self.manager_exists:
- # while event.level > 0:
- # event.pop_level()
-
- # if not event.name.startswith("Pygame") and event.name not in {"tick", "gameboard_create_piece", "server->create_piece", "create_piece->network"}:
- # print(f'''{self.__class__.__name__}({self.name!r}):\n{event = }''')
-
- # Call all registered handlers for this event
- if event.name in self.__event_handlers:
- for handler, _name in self.__event_handlers[event.name]:
- nursery.start_soon(handler, event)
-
- # Forward events to contained managers
- for component in self.get_all_components():
- # Skip self component if exists
- if component is self:
- continue
- if isinstance(component, ComponentManager):
- nursery.start_soon(component.raise_event, event)
-
- async def raise_event(self, event: Event[Any]) -> None:
- """Raise event for all components that have handlers registered."""
- async with trio.open_nursery() as nursery:
- await self.raise_event_in_nursery(event, nursery)
-
- def add_component(self, component: Component) -> None:
- """Add component to this manager.
-
- Raises ValueError if component already exists with component name.
- `component` must be an instance of Component.
- """
- assert isinstance(component, Component), "Must be component instance"
- if self.component_exists(component.name):
- raise ValueError(
- f'Component named "{component.name}" already exists!',
- )
- self.__components[component.name] = component
- component.bind(self)
-
- def add_components(self, components: Iterable[Component]) -> None:
- """Add multiple components to this manager.
-
- Raises ValueError if any component already exists with component name.
- `component`s must be instances of Component.
- """
- for component in components:
- self.add_component(component)
-
- def remove_component(self, component_name: object) -> None:
- """Remove a component.
-
- Raises ValueError if component name does not exist.
- """
- if not self.component_exists(component_name):
- raise ValueError(f"Component {component_name!r} does not exist!")
- # Remove component from registered components
- component = self.__components.pop(component_name)
- # Tell component they need to unbind
- component._unbind()
-
- # Unregister component's event handlers
- # List of events that will have no handlers once we are done
- empty = []
- for event_name, handlers in self.__event_handlers.items():
- for item in tuple(handlers):
- _handler, handler_component = item
- if handler_component == component_name:
- self.__event_handlers[event_name].remove(item)
- if not self.__event_handlers[event_name]:
- empty.append(event_name)
- # Remove event handler table keys that have no items anymore
- for name in empty:
- self.__event_handlers.pop(name)
-
- def component_exists(self, component_name: object) -> bool:
- """Return if component exists in this manager."""
- return component_name in self.__components
-
- @contextmanager
- def temporary_component(
- self,
- component: ComponentPassthrough,
- ) -> Generator[ComponentPassthrough, None, None]:
- """Temporarily add given component but then remove after exit."""
- name = component.name
- self.add_component(component)
- try:
- yield component
- finally:
- if self.component_exists(name):
- self.remove_component(name)
-
- def components_exist(self, component_names: Iterable[object]) -> bool:
- """Return if all component names given exist in this manager."""
- return all(self.component_exists(name) for name in component_names)
-
- def get_component(self, component_name: object) -> Any:
- """Return Component or raise ValueError because it doesn't exist."""
- if not self.component_exists(component_name):
- raise ValueError(f'"{component_name}" component does not exist')
- return self.__components[component_name]
-
- def get_components(self, component_names: Iterable[object]) -> list[Any]:
- """Return iterable of components asked for or raise ValueError."""
- return [self.get_component(name) for name in component_names]
-
- def list_components(self) -> tuple[object, ...]:
- """Return tuple of the names of components bound to this manager."""
- return tuple(self.__components)
-
- def get_all_components(self) -> tuple[Component, ...]:
- """Return tuple of all components bound to this manager."""
- return tuple(self.__components.values())
-
- def unbind_components(self) -> None:
- """Unbind all components, allows things to get garbage collected."""
- self.__event_handlers.clear()
- for component in iter(self.__components.values()):
- if (
- isinstance(component, ComponentManager)
- and component is not self
- ):
- component.unbind_components()
- component._unbind()
- self.__components.clear()
-
- def __del__(self) -> None:
- """Unbind components."""
- self.unbind_components()
-
-
-class ExternalRaiseManager(ComponentManager):
- """Component Manager, but raises events in an external nursery."""
-
- __slots__ = ("nursery",)
-
- def __init__(
- self,
- name: object,
- nursery: trio.Nursery,
- own_name: object | None = None,
- ) -> None:
- """Initialize with name, own component name, and nursery."""
- super().__init__(name, own_name)
- self.nursery = nursery
-
- async def raise_event(self, event: Event[Any]) -> None:
- """Raise event in nursery.
-
- Could raise RuntimeError if self.nursery is no longer open.
- """
- await self.raise_event_in_nursery(event, self.nursery)
-
- async def raise_event_internal(self, event: Event[Any]) -> None:
- """Raise event in internal nursery."""
- await super().raise_event(event)
-
-
-if __name__ == "__main__": # pragma: nocover
- print(f"{__title__}\nProgrammed by {__author__}.")
diff --git a/src/azul/encrypted_event.py b/src/azul/encrypted_event.py
deleted file mode 100644
index a34b046..0000000
--- a/src/azul/encrypted_event.py
+++ /dev/null
@@ -1,130 +0,0 @@
-"""Encrypted Event - Encrypt and decrypt event data."""
-
-# Programmed by CoolCat467
-
-from __future__ import annotations
-
-# Encrypted Event - Encrypt and decrypt event data.
-# Copyright (C) 2024 CoolCat467
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-__title__ = "Encrypted Event"
-__author__ = "CoolCat467, ItsDrike, and Ammar Askar"
-__version__ = "0.0.0"
-__license__ = "GNU General Public License Version 3"
-
-
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives.ciphers import (
- Cipher,
- CipherContext,
- algorithms,
- modes,
-)
-
-from azul.network import NetworkEventComponent
-
-
-class EncryptedNetworkEventComponent(NetworkEventComponent):
- """Encrypted Network Event Component."""
-
- __slots__ = (
- "cipher",
- "decryptor",
- "encryptor",
- "shared_secret",
- )
-
- def __init__(self, name: str) -> None:
- """Initialize Encrypted Network Event Component."""
- super().__init__(name)
-
- self.cipher: Cipher[modes.CFB8] | None = None
- self.encryptor: CipherContext
- self.decryptor: CipherContext
-
- @property
- def encryption_enabled(self) -> bool:
- """Return if encryption is enabled."""
- return self.cipher is not None
-
- def enable_encryption(
- self,
- shared_secret: bytes,
- initialization_vector: bytes,
- ) -> None:
- """Enable encryption for this connection, using the ``shared_secret``.
-
- After calling this method, the reading and writing process for this connection
- will be altered, and any future communication will be encrypted/decrypted there.
-
- :param shared_secret:
- This is the cipher key for the AES symmetric cipher used for the encryption.
-
- See :func:`azul.encryption.generate_shared_secret`.
- """
- self.cipher = Cipher(
- algorithms.AES256(bytes(shared_secret)),
- modes.CFB8(bytes(initialization_vector)),
- backend=default_backend(),
- )
- self.encryptor = self.cipher.encryptor()
- self.decryptor = self.cipher.decryptor()
-
- async def write(self, data: bytes) -> None:
- """Send the given data, encrypted through the stream, blocking if necessary.
-
- Args:
- data (bytes, bytearray, or memoryview): The data to send.
-
- Raises:
- trio.BusyResourceError: if another task is already executing a
- :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or
- :meth:`HalfCloseableStream.send_eof` on this stream.
- trio.BrokenResourceError: if something has gone wrong, and the stream
- is broken.
- trio.ClosedResourceError: if you previously closed this stream
- object, or if another task closes this stream object while
- :meth:`send_all` is running.
-
- Most low-level operations in Trio provide a guarantee: if they raise
- :exc:`trio.Cancelled`, this means that they had no effect, so the
- system remains in a known state. This is **not true** for
- :meth:`send_all`. If this operation raises :exc:`trio.Cancelled` (or
- any other exception for that matter), then it may have sent some, all,
- or none of the requested data, and there is no way to know which.
-
- Copied from Trio docs.
-
- """
- if self.encryption_enabled:
- data = self.encryptor.update(data)
- return await super().write(data)
-
- async def read(self, length: int) -> bytearray:
- """Read `length` bytes from stream.
-
- Can raise following exceptions:
- NetworkStreamNotConnectedError
- NetworkTimeoutError - Timeout or no data
- OSError - Stopped responding
- trio.BusyResourceError - Another task is already writing data
- trio.BrokenResourceError - Something is wrong and stream is broken
- trio.ClosedResourceError - Stream is closed or another task closes stream
- """
- data = await super().read(length)
- if self.encryption_enabled:
- return bytearray(self.decryptor.update(data))
- return data
diff --git a/src/azul/encryption.py b/src/azul/encryption.py
deleted file mode 100644
index e2ad0d2..0000000
--- a/src/azul/encryption.py
+++ /dev/null
@@ -1,140 +0,0 @@
-"""Encryption module."""
-
-# This is the buffer module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-import os
-
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP
-from cryptography.hazmat.primitives.asymmetric.rsa import (
- RSAPrivateKey as RSAPrivateKey,
- RSAPublicKey as RSAPublicKey,
- generate_private_key,
-)
-from cryptography.hazmat.primitives.hashes import SHA256
-from cryptography.hazmat.primitives.serialization import (
- Encoding,
- PublicFormat,
- load_der_public_key,
-)
-
-
-def generate_shared_secret() -> bytes: # pragma: no cover
- """Generate a random shared secret for client.
-
- This secret will be sent to the server in :class:`~mcproto.packets.login.login.LoginEncryptionResponse` packet,
- and used to encrypt all future communication afterwards.
-
- This will be symmetric encryption using AES/CFB8 stream cipher. And this shared secret will be 256-bits long.
- """
- return os.urandom(256 // 8)
-
-
-def generate_verify_token() -> bytes: # pragma: no cover
- """Generate a random verify token.
-
- This token will be sent by the server in :class:`~mcproto.packets.login.login.LoginEncryptionRequest`, to be
- encrypted by the client as a form of verification.
-
- This token doesn't need to be cryptographically secure, it's just a sanity check that
- the client has encrypted the data correctly.
- """
- return os.urandom(16)
-
-
-def generate_rsa_key() -> RSAPrivateKey: # pragma: no cover
- """Generate a random RSA key pair for server.
-
- This key pair will be used for :class:`~mcproto.packets.login.login.LoginEncryptionRequest` packet,
- where the client will be sent the public part of this key pair, which will be used to encrypt the
- shared secret (and verification token) sent in :class:`~mcproto.packets.login.login.LoginEncryptionResponse`
- packet. The server will then use the private part of this key pair to decrypt that.
-
- This will be a 2048-bit RSA key pair.
- """
- return generate_private_key(
- public_exponent=65537,
- key_size=2048,
- backend=default_backend(),
- )
-
-
-def encrypt_with_rsa(
- public_key: RSAPublicKey,
- data: bytes,
-) -> bytes:
- """Encrypt given data with given RSA public key."""
- return public_key.encrypt(
- bytes(data),
- OAEP(MGF1(SHA256()), SHA256(), None),
- )
-
-
-def encrypt_token_and_secret(
- public_key: RSAPublicKey,
- verification_token: bytes,
- shared_secret: bytes,
-) -> tuple[bytes, bytes]:
- """Encrypts the verification token and shared secret with the server's public key.
-
- :param public_key: The RSA public key provided by the server
- :param verification_token: The verification token provided by the server
- :param shared_secret: The generated shared secret
- :return: A tuple containing (encrypted token, encrypted secret)
- """
- encrypted_token = encrypt_with_rsa(public_key, verification_token)
- encrypted_secret = encrypt_with_rsa(public_key, shared_secret)
- return encrypted_token, encrypted_secret
-
-
-def decrypt_with_rsa(
- private_key: RSAPrivateKey,
- data: bytes,
-) -> bytes:
- """Decrypt given data with given RSA private key."""
- return private_key.decrypt(
- bytes(data),
- OAEP(MGF1(SHA256()), SHA256(), None),
- )
-
-
-def decrypt_token_and_secret(
- private_key: RSAPrivateKey,
- verification_token: bytes,
- shared_secret: bytes,
-) -> tuple[bytes, bytes]:
- """Decrypts the verification token and shared secret with the server's private key.
-
- :param private_key: The RSA private key generated by the server
- :param verification_token: The verification token encrypted and sent by the client
- :param shared_secret: The shared secret encrypted and sent by the client
- :return: A tuple containing (decrypted token, decrypted secret)
- """
- decrypted_token = decrypt_with_rsa(private_key, verification_token)
- decrypted_secret = decrypt_with_rsa(private_key, shared_secret)
- return decrypted_token, decrypted_secret
-
-
-def serialize_public_key(
- public_key: RSAPublicKey,
-) -> bytes:
- """Return public key serialize as bytes."""
- return public_key.public_bytes(
- encoding=Encoding.DER,
- format=PublicFormat.SubjectPublicKeyInfo,
- )
-
-
-def deserialize_public_key(serialized_public_key: bytes) -> RSAPublicKey:
- """Return deserialized public key."""
- # Key type is determined by the passed key itself.
- # Should be be an RSA public key in this case.
- key = load_der_public_key(serialized_public_key, default_backend())
- assert isinstance(key, RSAPublicKey)
- return key
diff --git a/src/azul/game.py b/src/azul/game.py
index 4b9708f..db25d88 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -35,10 +35,16 @@
from collections import Counter
from functools import lru_cache, wraps
from pathlib import Path
-from typing import TYPE_CHECKING, Final, TypeVar
+from typing import TYPE_CHECKING, Any, Final, TypeVar
import pygame
import trio
+from libcomponent.component import (
+ ComponentManager,
+ Event,
+ ExternalRaiseManager,
+)
+from libcomponent.network_utils import find_ip
from numpy import array, int8
from pygame.color import Color
from pygame.locals import (
@@ -55,12 +61,7 @@
from azul import element_list, objects, sprite
from azul.async_clock import Clock
from azul.client import GameClient, read_advertisements
-from azul.component import (
- ComponentManager,
- Event,
- ExternalRaiseManager,
-)
-from azul.network_shared import DEFAULT_PORT, find_ip
+from azul.network_shared import DEFAULT_PORT
from azul.server import GameServer
from azul.sound import SoundData, play_sound as base_play_sound
from azul.state import Tile
@@ -71,6 +72,9 @@
)
from azul.vector import Vector2
+if sys.version_info < (3, 11):
+ from exceptiongroup import ExceptionGroup
+
if TYPE_CHECKING:
from collections.abc import (
Awaitable,
@@ -395,21 +399,21 @@ class ObjectHandler:
def __init__(self) -> None:
"""Initialize object handler."""
- self.objects: dict[int, Object] = {}
+ self.objects: dict[int, sprite.Sprite] = {}
self.next_id = 0
self.cache: dict[str, int] = {}
self.recalculate_render = True
self._render_order: tuple[int, ...] = ()
- def add_object(self, obj: Object) -> None:
+ def add_object(self, obj: sprite.Sprite) -> None:
"""Add an object to the game."""
obj.id = self.next_id
self.objects[self.next_id] = obj
self.next_id += 1
self.recalculate_render = True
- def rm_object(self, obj: Object) -> None:
+ def rm_object(self, obj: sprite.Sprite) -> None:
"""Remove an object from the game."""
del self.objects[obj.id]
self.recalculate_render = True
@@ -420,7 +424,7 @@ def rm_star(self) -> None:
self.rm_object(self.objects[oid])
self.next_id = 0
- def get_object(self, object_id: int) -> Object | None:
+ def get_object(self, object_id: int) -> sprite.Sprite | None:
"""Return the object associated with object id given. Return None if object not found."""
if object_id in self.objects:
return self.objects[object_id]
@@ -454,17 +458,17 @@ def reset_cache(self) -> None:
"""Reset the cache."""
self.cache = {}
- def get_object_by_name(self, object_name: str) -> Object:
+ def get_object_by_name(self, object_name: str) -> sprite.Sprite:
"""Get object by name, with cache."""
if object_name not in self.cache:
ids = self.get_object_given_name(object_name)
if ids:
self.cache[object_name] = min(ids)
else:
- raise RuntimeError(f"{object_name} Object Not Found!")
+ raise RuntimeError(f"{object_name} sprite.Sprite Not Found!")
result = self.get_object(self.cache[object_name])
if result is None:
- raise RuntimeError(f"{object_name} Object Not Found!")
+ raise RuntimeError(f"{object_name} sprite.Sprite Not Found!")
return result
def set_attr_all(self, attribute: str, value: object) -> None:
@@ -550,10 +554,10 @@ def __del__(self) -> None:
class MultipartObject(ObjectHandler):
- """Thing that is both an Object and an ObjectHandler, and is meant to be an Object made up of multiple Objects."""
+ """Thing that is both an sprite.Sprite and an ObjectHandler, and is meant to be an sprite.Sprite made up of multiple Objects."""
def __init__(self, name: str):
- """Initialize Object and ObjectHandler of self.
+ """Initialize sprite.Sprite and ObjectHandler of self.
Also set self._lastloc and self._lasthidden to None
"""
@@ -584,8 +588,8 @@ def get_intersection(
return None, None
def process(self, time_passed: float) -> None:
- """Process Object self and ObjectHandler self and call self.reset_position on location change."""
- Object.process(self, time_passed)
+ """Process sprite.Sprite self and ObjectHandler self and call self.reset_position on location change."""
+ sprite.Sprite.process(self, time_passed)
ObjectHandler.process_objects(self, time_passed)
if self.location != self._lastloc:
@@ -598,12 +602,12 @@ def process(self, time_passed: float) -> None:
def render(self, surface: pygame.surface.Surface) -> None:
"""Render self and all parts to the surface."""
- Object.render(self, surface)
+ sprite.Sprite.render(self, surface)
ObjectHandler.render_objects(self, surface)
def __del__(self) -> None:
"""Delete data."""
- Object.__del__(self)
+ sprite.Sprite.__del__(self)
ObjectHandler.__del__(self)
@@ -760,7 +764,7 @@ def update_image(self) -> None:
self.dirty = 1
self.visible = bool(tile_count)
- def bind_handlers(self):
+ def bind_handlers(self) -> None:
"""Register handlers."""
self.register_handlers(
{
@@ -1271,7 +1275,7 @@ def get_tile(self, replace: int = Tile.blank) -> int:
return self.tiles.pop()
def place_tile(self, tile: int) -> None:
- """Place a given int Object on self if permitted."""
+ """Place a given int sprite.Sprite on self if permitted."""
if self.can_place(tile):
self.color = tile.color
self.tiles.append(tile)
@@ -1369,7 +1373,7 @@ def reset_position(self) -> None:
w = self.get_row(last - 1).width_height[0]
if w is None:
raise RuntimeError(
- "Image Dimensions for Row Object (row.width_height) are None!",
+ "Image Dimensions for Row sprite.Sprite (row.width_height) are None!",
)
h1 = self.get_row(0).tile_full
h = int(last * h1)
@@ -1424,7 +1428,7 @@ def __init__(self, player: Player) -> None:
self.name = "floor_line"
# self.font = Font(FONT, round(self.tile_size*1.2), color=BLACK, cx=False, cy=False)
- self.text = Text(
+ self.text = objects.Text(
round(self.tile_size * 1.2),
BLACK,
cx=False,
@@ -1432,8 +1436,7 @@ def __init__(self, player: Player) -> None:
)
self.has_number_one_tile = False
- gen = floor_line_subtract_generator(1)
- self.numbers = [next(gen) for i in range(self.size)]
+ self.numbers = [-255 for _ in range(self.size)]
def __repr__(self) -> str:
"""Return representation of self."""
@@ -1459,28 +1462,9 @@ def render(self, surface: pygame.surface.Surface) -> None:
# self.font.render(surface, str(self.numbers[x]), xy)
def place_tile(self, tile: int) -> None:
- """Place a given int Object on self if permitted."""
+ """Place a given int sprite.Sprite on self if permitted."""
self.tiles.insert(self.get_placed(), tile)
- if tile.color == self.number_one_color:
- self.has_number_one_tile = True
-
- box_lid = self.player.game.get_object_by_name("BoxLid")
- assert isinstance(box_lid, BoxLid)
-
- def handle_end(end: int) -> None:
- """Handle the end tile we are replacing. Ensures number one tile is not removed."""
- if not end.color < 0:
- if end.color == self.number_one_color:
- handle_end(self.tiles.pop())
- self.tiles.appendleft(end)
- return
- box_lid.add_tile(end)
-
- handle_end(self.tiles.pop())
-
- self.image_update = True
-
def score_tiles(self) -> int:
"""Score self.tiles and return how to change points."""
running_total = 0
@@ -1615,7 +1599,7 @@ def process(self, time_passed: float) -> None:
class Factories(MultipartObject):
- """Factories Multipart Object, made of multiple Factory Objects."""
+ """Factories Multipart sprite.Sprite, made of multiple Factory Objects."""
tiles_each = 4
@@ -1693,43 +1677,6 @@ def process(self, time_passed: float) -> None:
table.add_tiles(tocenter)
cursor.drag(select)
- def play_tiles_from_bag(self, empty_color: int = Tile.blank) -> None:
- """Divy up tiles to each factory from the bag."""
- # For every factory we have,
- for fid in range(self.count):
- # Draw tiles for the factory
- drawn = []
- for _i in range(self.tiles_each):
- # If the bag is not empty,
- if not self.game.bag.is_empty():
- # Draw a tile from the bag.
- tile = self.game.bag.draw_tile()
- assert tile is not None
- drawn.append(tile)
- else: # Otherwise, get the box lid
- box_lid = self.game.get_object_by_name("BoxLid")
- assert isinstance(box_lid, BoxLid)
- # If the box lid is not empty,
- if not box_lid.is_empty():
- # Add all the tiles from the box lid to the bag
- self.game.bag.add_tiles(box_lid.get_tiles())
- # and shake the bag to randomize everything
- self.game.bag.reset()
- # Then, grab a tile from the bag like usual.
- tile = self.game.bag.draw_tile()
- assert tile is not None
- drawn.append(tile)
- else:
- # "In the rare case that you run out of tiles again
- # while there are none left in the lid, start a new
- # round as usual even though are not all factory
- # displays are properly filled."
- drawn.append(int(empty_color))
- # Place drawn tiles on factory
- factory = self.objects[fid]
- assert isinstance(factory, Factory)
- factory.fill(drawn)
-
def is_all_empty(self) -> bool:
"""Return True if all factories are empty."""
for fid in range(self.count):
@@ -1741,7 +1688,7 @@ def is_all_empty(self) -> bool:
class TableCenter(TileRenderer):
- """Object that represents the center of the table."""
+ """sprite.Sprite that represents the center of the table."""
__slots__ = ("tiles",)
size = (6, 6)
@@ -1777,7 +1724,7 @@ def iter_tiles(self) -> Generator[int, None, None]:
for _ in range(one_count):
yield Tile.one
- def update_image(self):
+ def update_image(self) -> None:
"""Reset/update image."""
self.clear_image(self.size)
@@ -1852,7 +1799,7 @@ def __init__(
self.add_object(Board(self.varient_play))
self.add_object(PatternLine(self))
self.add_object(FloorLine(self))
- ## self.add_object(objects.Text(SCOREFONTSIZE, SCORECOLOR))
+ ## self.add_object(objects.objects.Text(SCOREFONTSIZE, SCORECOLOR))
self.score = 0
self.is_turn = False
@@ -1875,8 +1822,8 @@ def __repr__(self) -> str:
## def update_score(self) -> None:
## """Update the scorebox for this player."""
- ## score_box = self.get_object_by_name("Text")
- ## assert isinstance(score_box, Text)
+ ## score_box = self.get_object_by_name("objects.Text")
+ ## assert isinstance(score_box, objects.Text)
## score_box.update_value(f"Player {self.player_id + 1}: {self.score}")
def trigger_turn_now(self) -> None:
@@ -1939,8 +1886,8 @@ def reset_position(self) -> None:
int(y + bh * (2 / 3)),
)
- text = self.get_object_by_name("Text")
- assert isinstance(text, Text)
+ text = self.get_object_by_name("objects.Text")
+ assert isinstance(text, objects.Text)
text.location = Vector2(x - (bw // 3), y - (bh * 2 // 3))
@@ -2218,7 +2165,7 @@ async def set_state(*args: object, **kwargs: object) -> None:
class KwargOutlineText(objects.OutlinedText):
- """Outlined Text with attributes settable via keyword arguments."""
+ """Outlined objects.Text with attributes settable via keyword arguments."""
__slots__ = ()
@@ -2236,7 +2183,7 @@ def __init__(
class KwargButton(objects.Button):
- """Button with attributes settable via keyword arguments."""
+ """objects.Button with attributes settable via keyword arguments."""
__slots__ = ()
@@ -2272,7 +2219,7 @@ def add_button(
size: int = fontsize,
minlen: int = button_minimum,
) -> int:
- """Add a new Button object to group."""
+ """Add a new objects.Button object to group."""
button = KwargButton(
name,
font=pygame.font.Font(FONT, size),
@@ -2293,7 +2240,7 @@ def add_text(
size: int = fontsize,
outline: tuple[int, int, int] = BUTTON_TEXT_OUTLINE,
) -> int:
- """Add a new Text object to self.game with arguments. Return text id."""
+ """Add a new objects.Text object to self.game with arguments. Return text id."""
text = KwargOutlineText(
name,
font=pygame.font.Font(FONT, size),
@@ -2369,8 +2316,8 @@ def update_text(
def updater() -> None:
"""Update text object {text_name}'s value with {value_function}."""
assert self.game is not None
- text = self.game.get_object_by_name(f"Text{text_name}")
- assert isinstance(text, Text)
+ text = self.game.get_object_by_name(f"objects.Text{text_name}")
+ assert isinstance(text, objects.Text)
text.update_value(value_function())
return updater
@@ -2441,6 +2388,7 @@ async def mouse_moved(
self,
event: Event[sprite.PygameMouseMotion],
) -> None:
+ """Handle PygameMouseMotion event."""
## print(f'{event = }')
await self.manager.raise_event(
Event("cursor_set_location", event.data["pos"]),
@@ -2950,7 +2898,7 @@ def entry_actions(self) -> None:
(SCREEN_SIZE[0] // 2, SCREEN_SIZE[1] * 4 // 5),
)
buttontitle = self.game.get_object(bid)
- assert isinstance(buttontitle, Button)
+ assert isinstance(buttontitle, objects.Button)
buttontitle.Render_Priority = "last-1"
buttontitle.cur_time = 2
@@ -2961,7 +2909,7 @@ def entry_actions(self) -> None:
self.add_text(f"Line{idx}", line, (x, y), cx=True, cy=False)
# self.game.get_object(bid).Render_Priority = f'last{-(2+idx)}'
button = self.game.get_object(bid)
- assert isinstance(button, Button)
+ assert isinstance(button, objects.Button)
button.Render_Priority = "last-2"
y += self.bh
@@ -2974,8 +2922,6 @@ class Game(ObjectHandler):
def __init__(self) -> None:
"""Initialize game."""
super().__init__()
- # Gets overwritten by Keyboard object
- self.keyboard: Keyboard | None = None
self.states: dict[str, GameState] = {}
self.active_state: GameState | None = None
@@ -3006,9 +2952,6 @@ def __init__(self) -> None:
self.player_turn: int = 0
- # Tiles
- self.bag = Bag(TILECOUNT, REGTILECOUNT)
-
# # Cache
# self.cache: dict[int, pygame.surface.Surface] = {}
@@ -3016,7 +2959,7 @@ def __repr__(self) -> str:
"""Return representation of self."""
return f"{self.__class__.__name__}()"
- def add_object(self, obj: Object) -> None:
+ def add_object(self, obj: sprite.Sprite) -> None:
"""Add an object to the game."""
obj.game = self
super().add_object(obj)
@@ -3066,7 +3009,6 @@ def start_game(
self.add_object(Cursor(self))
self.add_object(TableCenter(self))
- self.add_object(BoxLid(self))
if self.is_host:
self.bag.reset()
diff --git a/src/azul/keyboard.py b/src/azul/keyboard.py
index daeda5b..a305365 100644
--- a/src/azul/keyboard.py
+++ b/src/azul/keyboard.py
@@ -9,7 +9,7 @@
__version__ = "0.0.0"
-from azul.component import ComponentManager
+from libcomponent.component import ComponentManager
class Keyboard(ComponentManager):
diff --git a/src/azul/mr_floppy_test.py b/src/azul/mr_floppy_test.py
index 03815e5..1b8a772 100644
--- a/src/azul/mr_floppy_test.py
+++ b/src/azul/mr_floppy_test.py
@@ -13,11 +13,11 @@
from typing import TYPE_CHECKING, Any, Final
import trio
+from libcomponent.component import Component, ComponentManager, Event
from pygame.locals import K_ESCAPE, KEYUP, QUIT, RESIZABLE, WINDOWRESIZED
from pygame.rect import Rect
from azul import conf, lang, objects, sprite
-from azul.component import Component, ComponentManager, Event
from azul.statemachine import AsyncState, AsyncStateMachine
from azul.vector import Vector2
diff --git a/src/azul/network.py b/src/azul/network.py
deleted file mode 100644
index fba2fb2..0000000
--- a/src/azul/network.py
+++ /dev/null
@@ -1,512 +0,0 @@
-"""Network - Module for sending events over the network."""
-
-# Programmed by CoolCat467
-
-from __future__ import annotations
-
-# Copyright (C) 2023-2024 CoolCat467
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-__title__ = "Network"
-__author__ = "CoolCat467"
-__license__ = "GNU General Public License Version 3"
-__version__ = "0.0.0"
-
-
-import contextlib
-from typing import (
- TYPE_CHECKING,
- Any,
- Literal,
- NoReturn,
-)
-
-import trio
-
-from azul.base_io import (
- BaseAsyncReader,
- BaseAsyncWriter,
- StructFormat,
-)
-from azul.buffer import Buffer
-from azul.component import (
- Component,
- ComponentManager,
- Event,
-)
-
-if TYPE_CHECKING:
- from types import TracebackType
-
- from typing_extensions import Self
-
-
-class NetworkTimeoutError(Exception):
- """Network Timeout Error."""
-
- __slots__ = ()
-
-
-class NetworkEOFError(Exception):
- """Network End of File Error."""
-
- __slots__ = ()
-
-
-class NetworkStreamNotConnectedError(Exception):
- """Network Stream Not Connected Error."""
-
- __slots__ = ()
-
-
-class NetworkComponent(Component, BaseAsyncReader, BaseAsyncWriter):
- """Network Component (client)."""
-
- __slots__ = ("_stream", "timeout")
-
- def __init__(self, name: str) -> None:
- """Initialize Network Component."""
- super().__init__(name)
-
- self.timeout: int | float = 3
- self._stream: trio.SocketStream | None = None
-
- @property
- def not_connected(self) -> bool:
- """Is stream None?."""
- return self._stream is None
-
- @property
- def stream(self) -> trio.SocketStream:
- """Trio SocketStream or raise NetworkStreamNotConnectedError."""
- if self._stream is None:
- raise NetworkStreamNotConnectedError("Stream not connected!")
- return self._stream
-
- @classmethod
- def from_stream(
- cls,
- *args: object,
- kwargs: dict[str, object] | None = None,
- stream: trio.SocketStream,
- ) -> Self:
- """Initialize from stream."""
- if kwargs is None:
- kwargs = {}
- self = cls(*args, **kwargs) # type: ignore[arg-type]
- self._stream = stream
- return self
-
- async def connect(self, host: str, port: int) -> None:
- """Connect to host:port on TCP.
-
- Raises:
- OSError: if the connection fails.
- RuntimeError: if stream is already connected
-
- """
- if not self.not_connected:
- raise RuntimeError("Already connected!")
- try: # pragma: nocover
- self._stream = await trio.open_tcp_stream(host, port)
- except OSError: # pragma: nocover
- await self.close()
- raise
-
- async def read(self, length: int) -> bytearray:
- """Read `length` bytes from stream.
-
- Can raise following exceptions:
- NetworkStreamNotConnectedError - Network stream is not connected
- NetworkTimeoutError - Timeout
- NetworkEOFError - End of File
- OSError - Stopped responding
- trio.BusyResourceError - Another task is already writing data
- trio.BrokenResourceError - Something is wrong and stream is broken
- trio.ClosedResourceError - Stream is closed or another task closes stream
- """
- content = bytearray()
- while max_read_count := length - len(content):
- received = b""
- # try:
- with trio.move_on_after(self.timeout) as cancel_scope:
- received = await self.stream.receive_some(max_read_count)
- cancel_called = cancel_scope.cancel_called
- # except (trio.BrokenResourceError, trio.ClosedResourceError):
- # await self.close()
- # raise
- if len(received) == 0:
- # No information at all
- if len(content) == 0:
- if cancel_called:
- raise NetworkTimeoutError("Read timed out.")
- raise NetworkEOFError(
- "Server did not respond with any information.",
- )
- # Only sent a few bytes, but we requested more
- raise OSError(
- f"Server stopped responding (got {len(content)} bytes, "
- f"but expected {length} bytes)."
- f" Partial obtained packet: {content!r}",
- )
- content.extend(received)
- return content
-
- async def write(self, data: bytes | bytearray | memoryview) -> None:
- """Send the given data through the stream, blocking if necessary.
-
- Args:
- data (bytes, bytearray, or memoryview): The data to send.
-
- Raises:
- trio.BusyResourceError: if another task is already executing a
- :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or
- :meth:`HalfCloseableStream.send_eof` on this stream.
- trio.BrokenResourceError: if something has gone wrong, and the stream
- is broken.
- trio.ClosedResourceError: if you previously closed this stream
- object, or if another task closes this stream object while
- :meth:`send_all` is running.
-
- Most low-level operations in Trio provide a guarantee: if they raise
- :exc:`trio.Cancelled`, this means that they had no effect, so the
- system remains in a known state. This is **not true** for
- :meth:`send_all`. If this operation raises :exc:`trio.Cancelled` (or
- any other exception for that matter), then it may have sent some, all,
- or none of the requested data, and there is no way to know which.
-
- Copied from Trio docs.
-
- """
- await self.stream.send_all(data)
-
- # try:
- # await self.stream.send_all(data)
- # except (trio.BrokenResourceError, trio.ClosedResourceError):
- # await self.close()
- # raise
-
- async def close(self) -> None:
- """Close the stream, possibly blocking."""
- if self._stream is None:
- await trio.lowlevel.checkpoint()
- return
- await self._stream.aclose()
- self._stream = None
-
- async def send_eof(self) -> None:
- """Close the sending half of the stream.
-
- This corresponds to ``shutdown(..., SHUT_WR)`` (`man
- page `__).
-
- If an EOF has already been sent, then this method should silently
- succeed.
-
- Raises:
- trio.BusyResourceError: if another task is already executing a
- :meth:`~SendStream.send_all`,
- :meth:`~SendStream.wait_send_all_might_not_block`, or
- :meth:`send_eof` on this stream.
- trio.BrokenResourceError: if something has gone wrong, and the stream
- is broken.
-
- Suppresses:
- trio.ClosedResourceError: if you previously closed this stream
- object, or if another task closes this stream object while
- :meth:`send_eof` is running.
-
- Copied from trio docs.
-
- """
- with contextlib.suppress(trio.ClosedResourceError):
- await self.stream.send_eof()
-
- async def wait_write_might_not_block(self) -> None:
- """Block until it's possible that :meth:`write` might not block.
-
- This method may return early: it's possible that after it returns,
- :meth:`send_all` will still block. (In the worst case, if no better
- implementation is available, then it might always return immediately
- without blocking. It's nice to do better than that when possible,
- though.)
-
- This method **must not** return *late*: if it's possible for
- :meth:`send_all` to complete without blocking, then it must
- return. When implementing it, err on the side of returning early.
-
- Raises:
- trio.BusyResourceError: if another task is already executing a
- :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or
- :meth:`HalfCloseableStream.send_eof` on this stream.
- trio.BrokenResourceError: if something has gone wrong, and the stream
- is broken.
- trio.ClosedResourceError: if you previously closed this stream
- object, or if another task closes this stream object while
- :meth:`wait_send_all_might_not_block` is running.
-
- Note:
- This method is intended to aid in implementing protocols that want
- to delay choosing which data to send until the last moment. E.g.,
- suppose you're working on an implementation of a remote display server
- like `VNC
- `__, and
- the network connection is currently backed up so that if you call
- :meth:`send_all` now then it will sit for 0.5 seconds before actually
- sending anything. In this case it doesn't make sense to take a
- screenshot, then wait 0.5 seconds, and then send it, because the
- screen will keep changing while you wait; it's better to wait 0.5
- seconds, then take the screenshot, and then send it, because this
- way the data you deliver will be more
- up-to-date. Using :meth:`wait_send_all_might_not_block` makes it
- possible to implement the better strategy.
-
- If you use this method, you might also want to read up on
- ``TCP_NOTSENT_LOWAT``.
-
- Further reading:
-
- * `Prioritization Only Works When There's Pending Data to Prioritize
- `__
-
- * WWDC 2015: Your App and Next Generation Networks: `slides
- `__,
- `video and transcript
- `__
-
- Copied from Trio docs.
-
- """
- return await self.stream.wait_send_all_might_not_block()
-
- async def __aenter__(self) -> Self:
- """Async context manager enter."""
- return self
-
- async def __aexit__(
- self,
- exc_type: type[BaseException] | None,
- exc_value: BaseException | None,
- traceback: TracebackType | None,
- ) -> None:
- """Async context manager exit. Close connection."""
- await self.close()
-
-
-# async def send_eof_and_close(self) -> None:
-# """Send EOF and close."""
-# await self.send_eof()
-# await self.close()
-
-
-class NetworkEventComponent(NetworkComponent):
- """Network Event Component - Send events over the network."""
-
- __slots__ = (
- "_read_packet_id_to_event_name",
- "_write_event_name_to_packet_id",
- "read_lock",
- "write_lock",
- )
-
- # Max of 255 packet ids
- # Next higher is USHORT with 65535 packet ids
- packet_id_format: Literal[StructFormat.UBYTE] = StructFormat.UBYTE
-
- def __init__(self, name: str) -> None:
- """Initialize Network Event Component."""
- super().__init__(name)
- self._read_packet_id_to_event_name: dict[int, str] = {}
- self._write_event_name_to_packet_id: dict[str, int] = {}
- self.read_lock = trio.Lock()
- self.write_lock = trio.Lock()
-
- def bind_handlers(self) -> None:
- """Register serverbound event handlers."""
- self.register_handlers(
- dict.fromkeys(
- self._write_event_name_to_packet_id,
- self.write_event,
- ),
- )
-
- def register_network_write_event(
- self,
- event_name: str,
- packet_id: int,
- ) -> None:
- """Map event name to serverbound packet id.
-
- Raises:
- ValueError: Event name already registered or infinite network loop.
-
- """
- if event_name in self._write_event_name_to_packet_id:
- raise ValueError(f"{event_name!r} event already registered!")
- if self._read_packet_id_to_event_name.get(packet_id) == event_name:
- raise ValueError(
- f"{event_name!r} events are also being received "
- f"from server with packet id {packet_id!r}, "
- "which will would lead to infinite looping over network",
- )
- self._write_event_name_to_packet_id[event_name] = packet_id
- if self.manager_exists:
- self.register_handler(event_name, self.write_event)
-
- def register_network_write_events(self, event_map: dict[str, int]) -> None:
- """Map event names to serverbound packet ids."""
- for event_name, packet_id in event_map.items():
- self.register_network_write_event(event_name, packet_id)
-
- async def write_event(self, event: Event[bytearray]) -> None:
- """Send event to network.
-
- Raises:
- RuntimeError: if unregistered packet id received from network
- trio.BusyResourceError: if another task is already executing a
- :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or
- :meth:`HalfCloseableStream.send_eof` on this stream.
- trio.BrokenResourceError: if something has gone wrong, and the stream
- is broken.
- trio.ClosedResourceError: if you previously closed this stream
- object, or if another task closes this stream object while
- :meth:`send_all` is running.
-
- """
- packet_id = self._write_event_name_to_packet_id.get(event.name)
- if packet_id is None:
- raise RuntimeError(f"Unhandled network event name {event.name!r}")
- buffer = Buffer()
- buffer.write_value(self.packet_id_format, packet_id)
- buffer.write_bytearray(event.data)
- async with self.write_lock:
- await self.write(buffer)
-
- async def read_event(self) -> Event[bytearray]:
- """Receive event from network.
-
- Can raise following exceptions:
- RuntimeError - Unhandled packet id
- NetworkStreamNotConnectedError - Network stream is not connected
- NetworkTimeoutError - Timeout or no data
- OSError - Stopped responding
- trio.BrokenResourceError - Something is wrong and stream is broken
- trio.ClosedResourceError - Stream is closed or another task closes stream
-
- Shouldn't happen with write lock but still:
- trio.BusyResourceError - Another task is already writing data
- """
- async with self.read_lock:
- packet_id = await self.read_value(self.packet_id_format)
- event_data = await self.read_bytearray()
- event_name = self._read_packet_id_to_event_name.get(packet_id)
- if event_name is None:
- raise RuntimeError(f"Unhandled packet ID {packet_id!r}")
- return Event(event_name, event_data)
-
- def register_read_network_event(
- self,
- packet_id: int,
- event_name: str,
- ) -> None:
- """Map clientbound packet id to event name."""
- if packet_id in self._read_packet_id_to_event_name:
- raise ValueError(f"Packet ID {packet_id!r} already registered!")
- if self._write_event_name_to_packet_id.get(event_name) == packet_id:
- raise ValueError(
- f"Packet id {packet_id!r} packets are also being received "
- f"from server with as {event_name!r} events, "
- "which will would lead to infinite looping over network",
- )
- self._read_packet_id_to_event_name[packet_id] = event_name
-
- def register_read_network_events(self, packet_map: dict[int, str]) -> None:
- """Map clientbound packet ids to event names."""
- for packet_id, event_name in packet_map.items():
- self.register_read_network_event(packet_id, event_name)
-
-
-class Server(ComponentManager):
- """Asynchronous TCP Server."""
-
- __slots__ = ("serve_cancel_scope",)
-
- def __init__(self, name: str, own_name: str | None = None) -> None:
- """Initialize Server."""
- super().__init__(name, own_name)
- self.serve_cancel_scope: trio.CancelScope | None = None
-
- def stop_serving(self) -> None:
- """Cancel serve scope immediately.
-
- This method is idempotent, i.e., if the scope was already
- cancelled then this method silently does nothing.
- """
- if self.serve_cancel_scope is None:
- return
- self.serve_cancel_scope.cancel()
-
- # "Implicit return in function which does not return"
- async def serve( # type: ignore[misc] # pragma: nocover
- self,
- port: int,
- host: str | bytes | None = None,
- backlog: int | None = None,
- ) -> NoReturn:
- """Serve over TCP. See trio.open_tcp_listeners for argument details."""
- self.serve_cancel_scope = trio.CancelScope()
- async with trio.open_nursery() as nursery:
- listeners = await trio.open_tcp_listeners(
- port,
- host=host,
- backlog=backlog,
- )
-
- async def handle_serve(
- task_status: trio.TaskStatus[Any] = trio.TASK_STATUS_IGNORED,
- ) -> None:
- assert self.serve_cancel_scope is not None
- try:
- with self.serve_cancel_scope:
- await trio.serve_listeners(
- self.handler,
- listeners,
- handler_nursery=nursery,
- task_status=task_status,
- )
- except trio.Cancelled:
- # Close all listeners
- async with trio.open_nursery() as cancel_nursery:
- for listener in listeners:
- cancel_nursery.start_soon(listener.aclose)
-
- await nursery.start(handle_serve)
-
- async def handler(
- self,
- stream: trio.SocketStream,
- ) -> None: # pragma: nocover
- """Handle new client streams.
-
- Override in a subclass - Default only closes the stream
- """
- try:
- await stream.send_eof()
- finally:
- await stream.aclose()
-
-
-if __name__ == "__main__": # pragma: nocover
- print(f"{__title__}\nProgrammed by {__author__}.\n")
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index ce56c88..aa30f5d 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -27,7 +27,6 @@
from enum import IntEnum, auto
from typing import Final, NamedTuple, TypeAlias
-import trio
from mypy_extensions import u8
ADVERTISEMENT_IP: Final = "224.0.2.60"
@@ -45,36 +44,6 @@ class TickEventData(NamedTuple):
fps: float
-# Stolen from WOOF (Web Offer One File), Copyright (C) 2004-2009 Simon Budig,
-# available at http://www.home.unix-ag.org/simon/woof
-# with modifications
-
-# Utility function to guess the IP (as a string) where the server can be
-# reached from the outside. Quite nasty problem actually.
-
-
-async def find_ip() -> str: # pragma: nocover
- """Guess the IP where the server can be found from the network."""
- # we get a UDP-socket for the TEST-networks reserved by IANA.
- # It is highly unlikely, that there is special routing used
- # for these networks, hence the socket later should give us
- # the IP address of the default route.
- # We're doing multiple tests, to guard against the computer being
- # part of a test installation.
-
- candidates: list[str] = []
- for test_ip in ("192.0.2.0", "198.51.100.0", "203.0.113.0"):
- sock = trio.socket.socket(trio.socket.AF_INET, trio.socket.SOCK_DGRAM)
- await sock.connect((test_ip, 80))
- ip_addr: str = sock.getsockname()[0]
- sock.close()
- if ip_addr in candidates:
- return ip_addr
- candidates.append(ip_addr)
-
- return candidates[0]
-
-
class ClientBoundEvents(IntEnum):
"""Client bound event IDs."""
diff --git a/src/azul/objects.py b/src/azul/objects.py
index 0f89f07..77d71a8 100644
--- a/src/azul/objects.py
+++ b/src/azul/objects.py
@@ -35,10 +35,9 @@
from azul import sprite
if TYPE_CHECKING:
+ from libcomponent.component import Event
from pygame.font import Font
- from azul.component import Event
-
class Text(sprite.Sprite):
"""Text element.
diff --git a/src/azul/server.py b/src/azul/server.py
index 3769b39..f361f62 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -34,26 +34,25 @@
from typing import TYPE_CHECKING, NoReturn
import trio
-
-from azul import network
-from azul.base_io import StructFormat
-from azul.buffer import Buffer
-from azul.component import ComponentManager, Event, ExternalRaiseManager
-from azul.encrypted_event import EncryptedNetworkEventComponent
-from azul.encryption import (
- RSAPrivateKey,
- decrypt_token_and_secret,
- generate_rsa_key,
- generate_verify_token,
- serialize_public_key,
+from libcomponent import network
+from libcomponent.base_io import StructFormat
+from libcomponent.buffer import Buffer
+from libcomponent.component import (
+ ComponentManager,
+ Event,
+ ExternalRaiseManager,
+)
+from libcomponent.network_utils import (
+ ServerClientNetworkEventComponent,
+ find_ip,
)
+
from azul.network_shared import (
ADVERTISEMENT_IP,
ADVERTISEMENT_PORT,
DEFAULT_PORT,
ClientBoundEvents,
ServerBoundEvents,
- find_ip,
)
from azul.state import State
@@ -61,7 +60,7 @@
from collections.abc import Awaitable, Callable
-class ServerClient(EncryptedNetworkEventComponent):
+class ServerClient(ServerClientNetworkEventComponent):
"""Server Client Network Event Component.
When clients connect to server, this class handles the incoming
@@ -69,7 +68,7 @@ class ServerClient(EncryptedNetworkEventComponent):
that are transferred over the network.
"""
- __slots__ = ("client_id", "rsa_key", "verify_token")
+ __slots__ = ("client_id",)
def __init__(self, client_id: int) -> None:
"""Initialize Server Client."""
@@ -95,9 +94,6 @@ def __init__(self, client_id: int) -> None:
},
)
- self.rsa_key: RSAPrivateKey | None = None
- self.verify_token: bytes | None = None
-
def bind_handlers(self) -> None:
"""Bind event handlers."""
super().bind_handlers()
@@ -176,23 +172,8 @@ async def handle_callback_ping(
await self.write_callback_ping()
async def start_encryption_request(self) -> None:
- """Start encryption request and raise as server[write]->encryption_request."""
- if self.encryption_enabled:
- raise RuntimeError("Encryption is already set up!")
- self.rsa_key = generate_rsa_key()
- self.verify_token = generate_verify_token()
-
- public_key = self.rsa_key.public_key()
-
- serialized_public_key = serialize_public_key(public_key)
-
- buffer = Buffer()
- buffer.write_bytearray(serialized_public_key)
- buffer.write_bytearray(self.verify_token)
-
- await self.write_event(
- Event("server[write]->encryption_request", buffer),
- )
+ """Start encryption request and raise as `server[write]->encryption_request`."""
+ await super().start_encryption_request()
event = await self.read_event()
if event.name != f"client[{self.client_id}]->encryption_response":
@@ -201,36 +182,6 @@ async def start_encryption_request(self) -> None:
)
await self.handle_encryption_response(event)
- async def handle_encryption_response(
- self,
- event: Event[bytearray],
- ) -> None:
- """Read encryption response."""
- if self.rsa_key is None or self.verify_token is None:
- raise RuntimeError(
- "Was not expecting encryption response, request start not sent!",
- )
- if self.encryption_enabled:
- raise RuntimeError("Encryption is already set up!")
- buffer = Buffer(event.data)
-
- encrypted_shared_secret = buffer.read_bytearray()
- encrypted_verify_token = buffer.read_bytearray()
-
- verify_token, shared_secret = decrypt_token_and_secret(
- self.rsa_key,
- encrypted_verify_token,
- encrypted_shared_secret,
- )
-
- if verify_token != self.verify_token:
- raise RuntimeError(
- "Received verify token does not match sent verify token!",
- )
-
- # Start encrypting all future data
- self.enable_encryption(shared_secret, verify_token)
-
class GameServer(network.Server):
"""Checkers server.
@@ -261,7 +212,6 @@ def __init__(self, internal_singleplayer_mode: bool = False) -> None:
self.state = State.new_game(0)
self.client_players: dict[int, int] = {}
- self.player_selections: dict[int, Pos] = {}
self.players_can_interact: bool = False
self.internal_singleplayer_mode = internal_singleplayer_mode
@@ -382,7 +332,6 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]:
def new_game_init(self) -> None:
"""Start new game."""
self.client_players.clear()
- self.player_selections.clear()
## pieces = generate_pieces(*self.board_size)
self.state = State.new_game(self.client_count)
@@ -550,8 +499,7 @@ async def handler(self, stream: trio.SocketStream) -> None:
"""Accept clients. Called by network.Server.serve."""
if self.client_count == 0 and self.game_active():
# Old game was running but everyone left, restart
- self.state.pieces.clear()
- # self.state = CheckersState(self.board_size, {})
+ print("TODO: restart")
new_client_id = self.client_count
print(
f"{self.__class__.__name__}: client connected [client_id {new_client_id}]",
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index 1335e74..fbd1915 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -27,6 +27,7 @@
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, TypedDict, cast
import trio
+from libcomponent.component import Component, ComponentManager, Event
from pygame.color import Color
from pygame.event import Event as PygameEvent, event_name
from pygame.mask import Mask, from_surface as mask_from_surface
@@ -34,7 +35,6 @@
from pygame.sprite import LayeredDirty, LayeredUpdates, WeakDirtySprite
from pygame.surface import Surface
-from azul.component import Component, ComponentManager, Event
from azul.statemachine import AsyncStateMachine
from azul.vector import Vector2
diff --git a/src/azul/utils.py b/src/azul/utils.py
deleted file mode 100644
index 22bf7ec..0000000
--- a/src/azul/utils.py
+++ /dev/null
@@ -1,49 +0,0 @@
-"""Two's Complement Utilities."""
-
-# This is the base_io module from https://github.com/py-mine/mcproto v0.3.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-__all__ = ["from_twos_complement", "to_twos_complement"]
-
-
-def to_twos_complement(number: int, bits: int) -> int:
- """Convert a given ``number`` into twos complement format of given amount of ``bits``.
-
- :raises ValueError:
- Given ``number`` is out of range, and can't be converted into twos complement format, since
- it wouldn't fit into the given amount of ``bits``.
- """
- value_max = 1 << (bits - 1)
- value_min = value_max * -1
- # With two's complement, we have one more negative number than positive
- # this means we can't be exactly at value_max, but we can be at exactly value_min
- if number >= value_max or number < value_min:
- raise ValueError(
- f"Can't convert number {number} into {bits}-bit twos complement format - out of range",
- )
-
- return number + (1 << bits) if number < 0 else number
-
-
-def from_twos_complement(number: int, bits: int) -> int:
- """Convert a given ``number`` from twos complement format of given amount of ``bits``.
-
- :raises ValueError:
- Given ``number`` doesn't fit into given amount of ``bits``. This likely means that you're using
- the wrong number, or that the number was converted into twos complement with higher amount of ``bits``.
- """
- value_max = (1 << bits) - 1
- if number < 0 or number > value_max:
- raise ValueError(
- f"Can't convert number {number} from {bits}-bit twos complement format - out of range",
- )
-
- if number & (1 << (bits - 1)) != 0:
- number -= 1 << bits
-
- return number
diff --git a/test-requirements.in b/test-requirements.in
index 449c3e8..6c3b0de 100644
--- a/test-requirements.in
+++ b/test-requirements.in
@@ -18,8 +18,8 @@ typing-extensions
# Azul's own dependencies
#
-cryptography>=43.0.0
exceptiongroup; python_version < '3.11'
+libcomponent~=0.0.0
mypy_extensions>=1.0.0
numpy~=2.1.3
pygame~=2.6.0
diff --git a/tests/helpers.py b/tests/helpers.py
deleted file mode 100644
index 24390e4..0000000
--- a/tests/helpers.py
+++ /dev/null
@@ -1,175 +0,0 @@
-# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-import inspect
-import unittest.mock
-from functools import partial
-from typing import TYPE_CHECKING, Any, Generic, TypeVar
-
-import trio
-from typing_extensions import ParamSpec
-
-if TYPE_CHECKING:
- from collections.abc import Callable, Coroutine
-
-T = TypeVar("T")
-P = ParamSpec("P")
-T_Mock = TypeVar("T_Mock", bound=unittest.mock.Mock)
-
-
-def synchronize(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
- """Take an asynchronous function, and return a synchronous alternative.
-
- This is needed because we sometimes want to test asynchronous behavior in a synchronous test function,
- where we can't simply await something. This function uses `trio.run` and generates a wrapper
- around the original asynchronous function, that awaits the result in a blocking synchronous way,
- returning the obtained value.
- """
-
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
- return trio.run(partial(f, *args, **kwargs))
-
- return wrapper
-
-
-class SynchronizedMixin:
- """Class acting as another wrapped object, with all async methods synchronized.
-
- This class needs :attr:`._WRAPPED_ATTRIBUTE` class variable to be set as the name of the internally
- held attribute, holding the object we'll be wrapping around.
-
- Child classes of this mixin will have their lookup logic changed, to instead perform a lookup
- on the wrapped attribute. Only if that lookup fails, we fallback to this class, meaning if both
- the wrapped attribute and this class have some attribute defined, the attribute from the wrapped
- object is returned. The only exceptions to this are lookup of the ``_WRAPPED_ATTRIBUTE`` variable,
- and of the attribute name stored under the ``_WRAPPED_ATTRIBUTE`` (the wrapped object).
-
- If the attribute held by the wrapped object is an asynchronous function, instead of returning it
- directly, the :func:`.synchronize` function will be called, returning a wrapped synchronous
- alternative for the requested async function.
-
- This is useful when we need to quickly create a synchronous alternative to a class holding async methods.
- However it isn't useful in production, since will cause typing issues (attributes will be accessible, but
- type checkers won't know that they exist here, because of the dynamic nature of this implementation).
- """
-
- _WRAPPED_ATTRIBUTE: str
-
- def __getattribute__(self, __name: str) -> Any:
- """Return attributes of the wrapped object, if the attribute is a coroutine function, synchronize it.
-
- The only exception to this behavior is getting the :attr:`._WRAPPED_ATTRIBUTE` variable itself, or the
- attribute named as the content of the ``_WRAPPED_ATTRIBUTE`` variable. All other attribute access will
- be delegated to the wrapped attribute. If the wrapped object doesn't have given attribute, the lookup
- will fallback to regular lookup for variables belonging to this class.
- """
- if (
- __name == "_WRAPPED_ATTRIBUTE" or __name == self._WRAPPED_ATTRIBUTE
- ): # Order is important
- return super().__getattribute__(__name)
-
- wrapped = getattr(self, self._WRAPPED_ATTRIBUTE)
-
- if hasattr(wrapped, __name):
- obj = getattr(wrapped, __name)
- if inspect.iscoroutinefunction(obj):
- return synchronize(obj)
- return obj
-
- return super().__getattribute__(__name)
-
- def __setattr__(self, __name: str, __value: object) -> None:
- """Allow for changing attributes of the wrapped object.
-
- * If wrapped object isn't yet set, fall back to :meth:`~object.__setattr__` of this class.
- * If wrapped object doesn't already contain the attribute we want to set, also fallback to this class.
- * Otherwise, run ``__setattr__`` on it to update it.
- """
- try:
- wrapped = getattr(self, self._WRAPPED_ATTRIBUTE)
- except AttributeError:
- return super().__setattr__(__name, __value)
- else:
- if hasattr(wrapped, __name):
- return setattr(wrapped, __name, __value)
-
- return super().__setattr__(__name, __value)
-
-
-class UnpropagatingMockMixin(Generic[T_Mock]):
- """Provides common functionality for our :class:`~unittest.mock.Mock` classes.
-
- By default, mock objects propagate themselves by returning a new instance of the same mock
- class, with same initialization attributes. This is done whenever we're accessing new
- attributes that mock class.
-
- This propagation makes sense for simple mocks without any additional restrictions, however when
- dealing with limited mocks to some ``spec_set``, it doesn't usually make sense to propagate
- those same ``spec_set`` restrictions, since we generally don't have attributes/methods of a
- class be of/return the same class.
-
- This mixin class stops this propagation, and instead returns instances of specified mock class,
- defined in :attr:`.child_mock_type` class variable, which is by default set to
- :class:`~unittest.mock.MagicMock`, as it can safely represent most objects.
-
- .. note:
- This propagation handling will only be done for the mock classes that inherited from this
- mixin class. That means if the :attr:`.child_mock_type` is one of the regular mock classes,
- and the mock is propagated, a regular mock class is returned as that new attribute. This
- regular class then won't have the same overrides, and will therefore propagate itself, like
- any other mock class would.
-
- If you wish to counteract this, you can set the :attr:`.child_mock_type` to a mock class
- that also inherits from this mixin class, perhaps to your class itself, overriding any
- propagation recursively.
- """
-
- child_mock_type: T_Mock = unittest.mock.MagicMock
-
- # Since this is a mixin class, we can access some attributes defined in mock classes safely.
- # Define the types of these variables here, for proper static type analysis.
- _mock_sealed: bool
- _extract_mock_name: Callable[[], str]
-
- def _get_child_mock(self, **kwargs) -> T_Mock:
- """Make :attr:`.child_mock_type`` instances instead of instances of the same class.
-
- By default, this method creates a new mock instance of the same original class, and passes
- over the same initialization arguments. This overrides that behavior to instead create an
- instance of :attr:`.child_mock_type` class.
- """
- # Mocks can be sealed, in which case we wouldn't want to allow propagation of any kind
- # and rather raise an AttributeError, informing that given attr isn't accessible
- if self._mock_sealed:
- mock_name = self._extract_mock_name()
- obj_name = (
- f"{mock_name}.{kwargs['name']}"
- if "name" in kwargs
- else f"{mock_name}()"
- )
- raise AttributeError(f"Can't access {obj_name}, mock is sealed.")
-
- # Propagate any other children as simple `unittest.mock.Mock` instances
- # rather than `self.__class__` instances
- return self.child_mock_type(**kwargs)
-
-
-class CustomMockMixin(UnpropagatingMockMixin):
- """Provides common functionality for our custom mock types.
-
- * Stops propagation of same ``spec_set`` restricted mock in child mocks
- (see :class:`.UnpropagatingMockMixin` for more info)
- * Allows using the ``spec_set`` attribute as class attribute
- """
-
- spec_set = None
-
- def __init__(self, **kwargs):
- if "spec_set" in kwargs:
- self.spec_set = kwargs.pop("spec_set")
- super().__init__(spec_set=self.spec_set, **kwargs) # type: ignore # Mixin class, this __init__ is valid
diff --git a/tests/protocol_helpers.py b/tests/protocol_helpers.py
deleted file mode 100644
index 74a16f9..0000000
--- a/tests/protocol_helpers.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-from unittest.mock import AsyncMock, Mock
-
-
-class WriteFunctionMock(Mock):
- """Mock write function, storing the written data."""
-
- def __init__(self, *a, **kw):
- super().__init__(*a, **kw)
- self.combined_data = bytearray()
-
- def __call__(
- self,
- data: bytes,
- ) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
- """Override mock's ``__call__`` to extend our :attr:`.combined_data` bytearray.
-
- This allows us to keep track of exactly what data was written by the mocked write function
- in total, rather than only having tools like :meth:`.assert_called_with`, which might let us
- get the data from individual calls, but not the combined data, which is what we'll need.
- """
- self.combined_data.extend(data)
- return super().__call__(data)
-
- def assert_has_data(
- self,
- data: bytearray,
- ensure_called: bool = True,
- ) -> None:
- """Ensure that the combined write data by the mocked function matches expected ``data``."""
- if ensure_called:
- self.assert_called()
-
- if self.combined_data != data:
- raise AssertionError(
- f"Write function mock expected data {data!r}, but was {self.call_data!r}",
- )
-
-
-class WriteFunctionAsyncMock(WriteFunctionMock, AsyncMock):
- """Asynchronous mock write function, storing the written data."""
-
-
-class ReadFunctionMock(Mock):
- """Mock read function, giving pre-defined data."""
-
- def __init__(self, *a, combined_data: bytearray | None = None, **kw):
- super().__init__(*a, **kw)
- if combined_data is None:
- combined_data = bytearray()
- self.combined_data = combined_data
-
- def __call__(
- self,
- length: int,
- ) -> bytearray: # pyright: ignore[reportIncompatibleMethodOverride]
- """Override mock's __call__ to make it return part of our :attr:`.combined_data` bytearray.
-
- This allows us to make the return value always be the next requested part (length) of
- the :attr:`.combined_data`. It would be difficult to replicate this with regular mocks,
- because some functions can end up making multiple read calls, and each time the result
- needs to be different (the next part).
- """
- self.return_value = self.combined_data[:length]
- del self.combined_data[:length]
- return super().__call__(length)
-
- def assert_read_everything(self, ensure_called: bool = True) -> None:
- """Ensure that the passed :attr:`.combined_data` was fully read and depleted."""
- if ensure_called:
- self.assert_called()
-
- if len(self.combined_data) != 0:
- raise AssertionError(
- f"Read function didn't deplete all of it's data, remaining data: {self.combined_data!r}",
- )
-
-
-class ReadFunctionAsyncMock(ReadFunctionMock, AsyncMock):
- """Asynchronous mock read function, giving pre-defined data."""
diff --git a/tests/test_base_io.py b/tests/test_base_io.py
deleted file mode 100644
index 456aad8..0000000
--- a/tests/test_base_io.py
+++ /dev/null
@@ -1,746 +0,0 @@
-# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-import platform
-import struct
-from abc import ABC, abstractmethod
-from typing import Any
-from unittest.mock import AsyncMock, Mock
-
-import pytest
-from helpers import SynchronizedMixin
-from protocol_helpers import (
- ReadFunctionAsyncMock,
- ReadFunctionMock,
- WriteFunctionAsyncMock,
- WriteFunctionMock,
-)
-
-from azul.base_io import (
- INT_FORMATS_TYPE,
- BaseAsyncReader,
- BaseAsyncWriter,
- BaseSyncReader,
- BaseSyncWriter,
- StructFormat,
-)
-from azul.utils import to_twos_complement
-
-# region: Initializable concrete implementations of ABC classes.
-
-
-class SyncWriter(BaseSyncWriter):
- """Initializable concrete implementation of :class:`~mcproto.protocol.base_io.BaseSyncWriter` ABC."""
-
- def write(self, data: bytes) -> None:
- """Concrete implementation of abstract write method.
-
- Since :class:`abc.ABC` classes can't be initialized if they have any abstract methods
- which weren't overridden with a concrete implementations, this is a fake implementation,
- without any actual logic, purely to allow the initialization of this class.
-
- This method is expected to be mocked using :class:`~tests.mcproto.protocol.helpers.WriteFunctionMock`
- if it's supposed to get called during testing.
-
- If this method gets called without being mocked, it will raise :exc:`NotImplementedError`.
- """
- raise NotImplementedError(
- "This concrete override of abstract write method isn't intended for actual use!\n"
- " - If you're writing a new test, did you forget to mock it?\n"
- " - If you're seeing this in an existing test, this method got called without the test expecting it,"
- " this probably means you changed something in the code leading to this call, but you haven't updated"
- " the tests to mock this function.",
- )
-
-
-class SyncReader(BaseSyncReader):
- """Testable concrete implementation of :class:`~mcproto.protocol.base_io.BaseSyncReader` ABC."""
-
- def read(self, length: int) -> bytearray:
- """Concrete implementation of abstract read method.
-
- Since :class:`abc.ABC` classes can't be initialized if they have any abstract methods
- which weren't overridden with a concrete implementations, this is a fake implementation,
- without any actual logic, purely to allow the initialization of this class.
-
- This method is expected to be mocked using :class:`~tests.mcproto.protocol.helpers.ReadFunctionMock`
- if it's supposed to get called during testing.
-
- If this method gets called without being mocked, it will raise :exc:`NotImplementedError`.
- """
- raise NotImplementedError(
- "This concrete override of abstract read method isn't intended for actual use!\n"
- " - If you're writing a new test, did you forget to mock it?\n"
- " - If you're seeing this in an existing test, this method got called without the test expecting it,"
- " this probably means you changed something in the code leading to this call, but you haven't updated"
- " the tests to mock this function.",
- )
-
-
-class AsyncWriter(BaseAsyncWriter):
- """Initializable concrete implementation of :class:`~mcproto.protocol.base_io.BaseAsyncWriter` ABC."""
-
- async def write(self, data: bytes) -> None:
- """Concrete implementation of abstract write method.
-
- Since :class:`abc.ABC` classes can't be initialized if they have any abstract methods
- which weren't overridden with a concrete implementations, this is a fake implementation,
- without any actual logic, purely to allow the initialization of this class.
-
- This method is expected to be mocked using :class:`~tests.mcproto.protocol.helpers.WriteFunctionAsyncMock`
- if it's supposed to get called during testing.
-
- If this method gets called without being mocked, it will raise :exc:`NotImplementedError`.
- """
- raise NotImplementedError(
- "This concrete override of abstract write method isn't intended for actual use!\n"
- " - If you're writing a new test, did you forget to mock it?\n"
- " - If you're seeing this in an existing test, this method got called without the test expecting it,"
- " this probably means you changed something in the code leading to this call, but you haven't updated"
- " the tests to mock this function.",
- )
-
-
-class AsyncReader(BaseAsyncReader):
- """Testable concrete implementation of BaseAsyncReader ABC."""
-
- async def read(self, length: int) -> bytearray:
- """Concrete implementation of abstract read method.
-
- Since :class:`abc.ABC` classes can't be initialized if they have any abstract methods
- which weren't overridden with a concrete implementations, this is a fake implementation,
- without any actual logic, purely to allow the initialization of this class.
-
- This method is expected to be mocked using :class:`~tests.mcproto.protocol.helpers.ReadFunctionAsyncMock`
- if it's supposed to get called during testing.
-
- If this method gets called without being mocked, it will raise :exc:`NotImplementedError`.
- """
- raise NotImplementedError(
- "This concrete override of abstract read method isn't intended for actual use!\n"
- " - If you're writing a new test, did you forget to mock it?\n"
- " - If you're seeing this in an existing test, this method got called without the test expecting it,"
- " this probably means you changed something in the code leading to this call, but you haven't updated"
- " the tests to mock this function.",
- )
-
-
-# endregion
-# region: Synchronized classes
-
-
-class WrappedAsyncReader(SynchronizedMixin):
- """Wrapped synchronous implementation of asynchronous :class:`.AsyncReader` class.
-
- This essentially mimics :class:`~mcproto.protocol.base_io.BaseSyncReader`.
- """
-
- _WRAPPED_ATTRIBUTE = "_reader"
-
- def __init__(self):
- self._reader = AsyncReader()
-
-
-class WrappedAsyncWriter(SynchronizedMixin):
- """Wrapped synchronous implementation of asynchronous :class:`.AsyncWriter` class.
-
- This essentially mimics :class:`~mcproto.protocol.base_io.BaseSyncWriter`.
- """
-
- _WRAPPED_ATTRIBUTE = "_writer"
-
- def __init__(self):
- self._writer = AsyncWriter()
-
-
-# endregion
-# region: Abstract test classes
-
-
-class WriterTests(ABC):
- """Collection of tests for both sync and async versions of the writer."""
-
- writer: BaseSyncWriter | BaseAsyncWriter
-
- @classmethod
- @abstractmethod
- def setup_class(cls):
- """Initialize writer instance to be tested."""
- ...
-
- @pytest.fixture
- def method_mock(self) -> Mock | AsyncMock:
- """Obtain the appropriate type of mock, supporting both sync and async modes."""
- if isinstance(self.writer, BaseSyncWriter):
- return Mock
- return AsyncMock
-
- @pytest.fixture
- def autopatch(self, monkeypatch: pytest.MonkeyPatch):
- """Create a simple function, supporting patching both sync/async writer functions with appropriate mocks.
-
- This returned function takes in the name of the function to patch, and returns the mock object.
- This mock object will either be Mock, or AsyncMock instance, depending on whether we're in async or sync mode.
- """
- if isinstance(self.writer, SyncWriter):
- patch_path = "mcproto.protocol.base_io.BaseSyncWriter"
- mock_type = Mock
- else:
- patch_path = "mcproto.protocol.base_io.BaseAsyncWriter"
- mock_type = AsyncMock
-
- def autopatch(function_name: str) -> Mock | AsyncMock:
- mock_f = mock_type()
- monkeypatch.setattr(f"{patch_path}.{function_name}", mock_f)
- return mock_f
-
- return autopatch
-
- @pytest.fixture
- def write_mock(self, monkeypatch: pytest.MonkeyPatch):
- """Monkeypatch the write function with a mock which is returned."""
- mock_f = (
- WriteFunctionMock()
- if isinstance(self.writer, BaseSyncWriter)
- else WriteFunctionAsyncMock()
- )
- monkeypatch.setattr(self.writer.__class__, "write", mock_f)
- return mock_f
-
- @pytest.mark.parametrize(
- ("fmt", "value", "expected_bytes"),
- [
- (StructFormat.UBYTE, 0, [0]),
- (StructFormat.UBYTE, 15, [15]),
- (StructFormat.UBYTE, 255, [255]),
- (StructFormat.BYTE, 0, [0]),
- (StructFormat.BYTE, 15, [15]),
- (StructFormat.BYTE, 127, [127]),
- (StructFormat.BYTE, -20, [to_twos_complement(-20, bits=8)]),
- (StructFormat.BYTE, -128, [to_twos_complement(-128, bits=8)]),
- ],
- )
- def test_write_value(
- self,
- fmt: INT_FORMATS_TYPE,
- value: Any,
- expected_bytes: list[int],
- write_mock: WriteFunctionMock,
- ):
- """Test writing values sends expected bytes."""
- self.writer.write_value(fmt, value)
- write_mock.assert_has_data(bytearray(expected_bytes))
-
- @pytest.mark.parametrize(
- ("fmt", "value"),
- [
- (StructFormat.UBYTE, -1),
- (StructFormat.UBYTE, 256),
- (StructFormat.BYTE, -129),
- (StructFormat.BYTE, 128),
- ],
- )
- def test_write_value_out_of_range(
- self,
- fmt: INT_FORMATS_TYPE,
- value: Any,
- ):
- """Test writing out of range values for the given format raises :exc:`struct.error`."""
- with pytest.raises(struct.error):
- self.writer.write_value(fmt, value)
-
- @pytest.mark.parametrize(
- ("number", "expected_bytes"),
- [
- (0, [0]),
- (1, [1]),
- (2, [2]),
- (15, [15]),
- (127, [127]),
- (128, [128, 1]),
- (129, [129, 1]),
- (255, [255, 1]),
- (1000000, [192, 132, 61]),
- (2147483647, [255, 255, 255, 255, 7]),
- ],
- )
- def test_write_varuint(
- self,
- number: int,
- expected_bytes: list[int],
- write_mock: WriteFunctionMock,
- ):
- """Test writing varuints results in correct bytes."""
- self.writer._write_varuint(number)
- write_mock.assert_has_data(bytearray(expected_bytes))
-
- @pytest.mark.parametrize(
- ("write_value", "max_bits"),
- [
- (-1, 128),
- (-1, 1),
- (2**16, 16),
- (2**32, 32),
- ],
- )
- def test_write_varuint_out_of_range(self, write_value: int, max_bits: int):
- """Test writing out of range varuints raises :exc:`ValueError`."""
- with pytest.raises(
- ValueError,
- match="^Tried to write varint outside of the range of",
- ):
- self.writer._write_varuint(write_value, max_bits=max_bits)
-
- @pytest.mark.parametrize(
- ("number", "expected_bytes"),
- [
- (127, [127]),
- (16384, [128, 128, 1]),
- (-128, [128, 255, 255, 255, 15]),
- (-16383, [129, 128, 255, 255, 15]),
- ],
- )
- def test_write_varint(
- self,
- number: int,
- expected_bytes: list[int],
- write_mock: WriteFunctionMock,
- ):
- """Test writing varints results in correct bytes."""
- self.writer.write_varint(number)
- write_mock.assert_has_data(bytearray(expected_bytes))
-
- @pytest.mark.parametrize(
- ("number", "expected_bytes"),
- [
- (127, [127]),
- (16384, [128, 128, 1]),
- (-128, [128, 255, 255, 255, 255, 255, 255, 255, 255, 1]),
- (-16383, [129, 128, 255, 255, 255, 255, 255, 255, 255, 1]),
- ],
- )
- def test_write_varlong(
- self,
- number: int,
- expected_bytes: list[int],
- write_mock: WriteFunctionMock,
- ):
- """Test writing varlongs results in correct bytes."""
- self.writer.write_varlong(number)
- write_mock.assert_has_data(bytearray(expected_bytes))
-
- @pytest.mark.parametrize(
- ("data", "expected_bytes"),
- [
- (b"", [0]),
- (b"\x01", [1, 1]),
- (
- b"hello\0world",
- [11, 104, 101, 108, 108, 111, 0, 119, 111, 114, 108, 100],
- ),
- (b"\x01\x02\x03four\x05", [8, 1, 2, 3, 102, 111, 117, 114, 5]),
- ],
- )
- def test_write_bytearray(
- self,
- data: bytes,
- expected_bytes: list[int],
- write_mock: WriteFunctionMock,
- ):
- """Test writing ASCII string results in correct bytes."""
- self.writer.write_bytearray(data)
- write_mock.assert_has_data(bytearray(expected_bytes))
-
- @pytest.mark.parametrize(
- ("string", "expected_bytes"),
- [
- ("test", [*list(map(ord, "test")), 0]),
- ("a" * 100, [*list(map(ord, "a" * 100)), 0]),
- ("", [0]),
- ],
- )
- def test_write_ascii(
- self,
- string: str,
- expected_bytes: list[int],
- write_mock: WriteFunctionMock,
- ):
- """Test writing ASCII string results in correct bytes."""
- self.writer.write_ascii(string)
- write_mock.assert_has_data(bytearray(expected_bytes))
-
- @pytest.mark.parametrize(
- ("string", "expected_bytes"),
- [
- ("test", [len("test"), *list(map(ord, "test"))]),
- ("a" * 100, [len("a" * 100), *list(map(ord, "a" * 100))]),
- ("", [0]),
- ("नमस्ते", [18] + [int(x) for x in "नमस्ते".encode()]),
- ],
- )
- def test_write_utf(
- self,
- string: str,
- expected_bytes: list[int],
- write_mock: WriteFunctionMock,
- ):
- """Test writing UTF string results in correct bytes."""
- self.writer.write_utf(string)
- write_mock.assert_has_data(bytearray(expected_bytes))
-
- @pytest.mark.skipif(
- platform.system() == "Windows",
- reason="environment variable limit on Windows",
- )
- def test_write_utf_limit(self, write_mock: WriteFunctionMock):
- """Test writing a UTF string too big raises a :exc:`ValueError`."""
- with pytest.raises(
- ValueError,
- match="Maximum character limit for writing strings is 32767 characters.",
- ):
- self.writer.write_utf("a" * (32768))
-
- def test_write_optional_true(
- self,
- method_mock: Mock | AsyncMock,
- write_mock: WriteFunctionMock,
- ):
- """Test writing non-``None`` value writes ``True`` and runs the writer function."""
- mock_v = Mock()
- mock_f = method_mock()
- self.writer.write_optional(mock_v, mock_f)
- mock_f.assert_called_once_with(mock_v)
- write_mock.assert_has_data(bytearray([1]))
-
- def test_write_optional_false(
- self,
- method_mock: Mock | AsyncMock,
- write_mock: WriteFunctionMock,
- ):
- """Test writing ``None`` value should write ``False`` and skip running the writer function."""
- mock_f = method_mock()
- self.writer.write_optional(None, mock_f)
- mock_f.assert_not_called()
- write_mock.assert_has_data(bytearray([0]))
-
-
-class ReaderTests(ABC):
- """Collection of tests for both sync and async versions of the reader."""
-
- reader: BaseSyncReader | BaseAsyncReader
-
- @classmethod
- @abstractmethod
- def setup_class(cls):
- """Initialize reader instance to be tested."""
- ...
-
- @pytest.fixture
- def method_mock(self) -> Mock | AsyncMock:
- """Obtain the appropriate type of mock, supporting both sync and async modes."""
- if isinstance(self.reader, BaseSyncReader):
- return Mock
- return AsyncMock
-
- @pytest.fixture
- def autopatch(self, monkeypatch: pytest.MonkeyPatch):
- """Create a simple function, supporting patching both sync/async reader functions with appropriate mocks.
-
- This returned function takes in the name of the function to patch, and returns the mock object.
- This mock object will either be Mock, or AsyncMock instance, depending on whether we're in async or sync mode.
- """
- if isinstance(self.reader, SyncReader):
- patch_path = "mcproto.protocol.base_io.BaseSyncReader"
- mock_type = Mock
- else:
- patch_path = "mcproto.protocol.base_io.BaseAsyncReader"
- mock_type = AsyncMock
-
- def autopatch(function_name: str) -> Mock | AsyncMock:
- mock_f = mock_type()
- monkeypatch.setattr(f"{patch_path}.{function_name}", mock_f)
- return mock_f
-
- return autopatch
-
- @pytest.fixture
- def read_mock(self, monkeypatch: pytest.MonkeyPatch):
- """Monkeypatch the read function with a mock which is returned."""
- mock_f = (
- ReadFunctionMock()
- if isinstance(self.reader, SyncReader)
- else ReadFunctionAsyncMock()
- )
- monkeypatch.setattr(self.reader.__class__, "read", mock_f)
- yield mock_f
- # Run this assertion after the test, to ensure that all specified data
- # to be read, actually was read
- mock_f.assert_read_everything()
-
- @pytest.mark.parametrize(
- ("fmt", "read_bytes", "expected_value"),
- [
- (StructFormat.UBYTE, [0], 0),
- (StructFormat.UBYTE, [10], 10),
- (StructFormat.UBYTE, [255], 255),
- (StructFormat.BYTE, [0], 0),
- (StructFormat.BYTE, [20], 20),
- (StructFormat.BYTE, [127], 127),
- (StructFormat.BYTE, [to_twos_complement(-20, bits=8)], -20),
- (StructFormat.BYTE, [to_twos_complement(-128, bits=8)], -128),
- ],
- )
- def test_read_value(
- self,
- fmt: INT_FORMATS_TYPE,
- read_bytes: list[int],
- expected_value: Any,
- read_mock: ReadFunctionMock,
- ):
- """Test reading bytes gets expected value."""
- read_mock.combined_data = bytearray(read_bytes)
- assert self.reader.read_value(fmt) == expected_value
-
- @pytest.mark.parametrize(
- ("read_bytes", "expected_value"),
- [
- ([0], 0),
- ([1], 1),
- ([2], 2),
- ([15], 15),
- ([127], 127),
- ([128, 1], 128),
- ([129, 1], 129),
- ([255, 1], 255),
- ([192, 132, 61], 1000000),
- ([255, 255, 255, 255, 7], 2147483647),
- ],
- )
- def test_read_varuint(
- self,
- read_bytes: list[int],
- expected_value: int,
- read_mock: ReadFunctionMock,
- ):
- """Test reading varuint bytes results in correct values."""
- read_mock.combined_data = bytearray(read_bytes)
- assert self.reader._read_varuint() == expected_value
-
- @pytest.mark.parametrize(
- ("read_bytes", "max_bits"),
- [
- ([128, 128, 4], 16),
- ([128, 128, 128, 128, 16], 32),
- ],
- )
- def test_read_varuint_out_of_range(
- self,
- read_bytes: list[int],
- max_bits: int,
- read_mock: ReadFunctionMock,
- ):
- """Test reading out-of-range varuints raises :exc:`IOError`."""
- read_mock.combined_data = bytearray(read_bytes)
- with pytest.raises(
- IOError,
- match="^Received varint was outside the range of",
- ):
- self.reader._read_varuint(max_bits=max_bits)
-
- @pytest.mark.parametrize(
- ("read_bytes", "expected_value"),
- [
- ([127], 127),
- ([128, 128, 1], 16384),
- ([128, 255, 255, 255, 15], -128),
- ([129, 128, 255, 255, 15], -16383),
- ],
- )
- def test_read_varint(
- self,
- read_bytes: list[int],
- expected_value: int,
- read_mock: ReadFunctionMock,
- ):
- """Test reading varuint bytes results in correct values."""
- read_mock.combined_data = bytearray(read_bytes)
- assert self.reader.read_varint() == expected_value
-
- @pytest.mark.parametrize(
- ("read_bytes", "expected_value"),
- [
- ([127], 127),
- ([128, 128, 1], 16384),
- ([128, 255, 255, 255, 255, 255, 255, 255, 255, 1], -128),
- ([129, 128, 255, 255, 255, 255, 255, 255, 255, 1], -16383),
- ],
- )
- def test_read_varlong(
- self,
- read_bytes: list[int],
- expected_value: int,
- read_mock: ReadFunctionMock,
- ):
- """Test reading varuint bytes results in correct values."""
- read_mock.combined_data = bytearray(read_bytes)
- assert self.reader.read_varlong() == expected_value
-
- @pytest.mark.parametrize(
- ("read_bytes", "expected_bytes"),
- [
- ([0], b""),
- ([1, 1], b"\x01"),
- (
- [11, 104, 101, 108, 108, 111, 0, 119, 111, 114, 108, 100],
- b"hello\0world",
- ),
- ([8, 1, 2, 3, 102, 111, 117, 114, 5], b"\x01\x02\x03four\x05"),
- ],
- )
- def test_read_bytearray(
- self,
- read_bytes: list[int],
- expected_bytes: bytes,
- read_mock: ReadFunctionMock,
- ):
- """Test reading ASCII string results in correct bytes."""
- read_mock.combined_data = bytearray(read_bytes)
- assert self.reader.read_bytearray() == expected_bytes
-
- @pytest.mark.parametrize(
- ("read_bytes", "expected_string"),
- [
- ([*list(map(ord, "test")), 0], "test"),
- ([*list(map(ord, "a" * 100)), 0], "a" * 100),
- ([0], ""),
- ],
- )
- def test_read_ascii(
- self,
- read_bytes: list[int],
- expected_string: str,
- read_mock: ReadFunctionMock,
- ):
- """Test reading ASCII string results in correct bytes."""
- read_mock.combined_data = bytearray(read_bytes)
- assert self.reader.read_ascii() == expected_string
-
- @pytest.mark.parametrize(
- ("read_bytes", "expected_string"),
- [
- ([len("test"), *list(map(ord, "test"))], "test"),
- ([len("a" * 100), *list(map(ord, "a" * 100))], "a" * 100),
- ([0], ""),
- ([18] + [int(x) for x in "नमस्ते".encode()], "नमस्ते"),
- ],
- )
- def test_read_utf(
- self,
- read_bytes: list[int],
- expected_string: str,
- read_mock: ReadFunctionMock,
- ):
- """Test reading UTF string results in correct values."""
- read_mock.combined_data = bytearray(read_bytes)
- assert self.reader.read_utf() == expected_string
-
- @pytest.mark.skipif(
- platform.system() == "Windows",
- reason="environment variable limit on Windows",
- )
- @pytest.mark.parametrize(
- ("read_bytes"),
- [
- [253, 255, 7],
- [128, 128, 2, *list(map(ord, "a" * 32768))],
- ],
- # Temporary workaround.
- # https://github.com/pytest-dev/pytest/issues/6881#issuecomment-596381626
- ids=["a", "b"],
- )
- def test_read_utf_limit(
- self,
- read_bytes: list[int],
- read_mock: ReadFunctionMock,
- ):
- """Test reading a UTF string too big raises an IOError."""
- read_mock.combined_data = bytearray(read_bytes)
- with pytest.raises(
- IOError,
- match="^Maximum read limit for utf strings is ",
- ):
- self.reader.read_utf()
-
- def test_read_optional_true(
- self,
- method_mock: Mock | AsyncMock,
- read_mock: ReadFunctionMock,
- ):
- """Test reading optional runs reader function when first bool is ``True``."""
- mock_f = method_mock()
- read_mock.combined_data = bytearray([1])
- self.reader.read_optional(mock_f)
- mock_f.assert_called_once_with()
-
- def test_read_optional_false(
- self,
- method_mock: Mock | AsyncMock,
- read_mock: ReadFunctionMock,
- ):
- """Test reading optional doesn't run reader function when first bool is ``False``."""
- mock_f = method_mock()
- read_mock.combined_data = bytearray([0])
- self.reader.read_optional(mock_f)
- mock_f.assert_not_called()
-
-
-# endregion
-# region: Concrete test classes
-
-
-class TestBaseSyncWriter(WriterTests):
- """Tests for individual write methods implemented in :class:`~mcproto.protocol.base_io.BaseSyncWriter`."""
-
- @classmethod
- def setup_class(cls):
- """Initialize writer instance to be tested."""
- cls.writer = SyncWriter()
-
-
-class TestBaseSyncReader(ReaderTests):
- """Tests for individual write methods implemented in :class:`~mcproto.protocol.base_io.BaseSyncReader`."""
-
- @classmethod
- def setup_class(cls):
- """Initialize reader instance to be tested."""
- cls.reader = SyncReader()
-
-
-class TestBaseAsyncWriter(WriterTests):
- """Tests for individual write methods implemented in :class:`~mcproto.protocol.base_io.BaseSyncReader`."""
-
- writer: WrappedAsyncWriter
-
- @classmethod
- def setup_class(cls):
- """Initialize writer instance to be tested."""
- cls.writer = WrappedAsyncWriter()
-
-
-class TestBaseAsyncReader(ReaderTests):
- """Tests for individual write methods implemented in :class:`~mcproto.protocol.base_io.BaseSyncReader`."""
-
- reader: WrappedAsyncReader
-
- @classmethod
- def setup_class(cls):
- """Initialize writer instance to be tested."""
- cls.reader = WrappedAsyncReader()
-
-
-# endregion
diff --git a/tests/test_buffer.py b/tests/test_buffer.py
deleted file mode 100644
index ac30774..0000000
--- a/tests/test_buffer.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-import pytest
-
-from azul.buffer import Buffer
-
-
-def test_write() -> None:
- """Writing into the buffer should store data."""
- buf = Buffer()
- buf.write(b"Hello")
- assert buf, bytearray(b"Hello")
-
-
-def test_read() -> None:
- """Reading from buffer should return stored data."""
- buf = Buffer(b"Reading is cool")
- data = buf.read(len(buf))
- assert data == b"Reading is cool"
-
-
-def test_read_multiple() -> None:
- """Multiple reads should deplete the data."""
- buf = Buffer(b"Something random")
- data = buf.read(9)
- assert data == b"Something"
- data = buf.read(7)
- assert data == b" random"
-
-
-def test_no_data_read() -> None:
- """Reading more data than available should raise IOError."""
- buf = Buffer(b"Blip")
- with pytest.raises(
- IOError,
- match="^Requested to read more data than available.",
- ):
- buf.read(len(buf) + 1)
-
-
-def test_reset() -> None:
- """Resetting should treat already read data as new unread data."""
- buf = Buffer(b"Will it reset?")
- data = buf.read(len(buf))
- buf.reset()
- data2 = buf.read(len(buf))
- assert data == data2
- assert data == b"Will it reset?"
-
-
-def test_clear() -> None:
- """Clearing should remove all stored data from buffer."""
- buf = Buffer(b"Will it clear?")
- buf.clear()
- assert buf == bytearray()
-
-
-def test_clear_resets_position() -> None:
- """Clearing should reset reading position for new data to be read."""
- buf = Buffer(b"abcdef")
- buf.read(3)
- buf.clear()
- buf.write(b"012345")
- data = buf.read(3)
- assert data == b"012"
-
-
-def test_clear_read_only() -> None:
- """Clearing should allow just removing the already read data."""
- buf = Buffer(b"0123456789")
- buf.read(5)
- buf.clear(only_already_read=True)
- assert buf == bytearray(b"56789")
-
-
-def test_flush() -> None:
- """Flushing should read all available data and clear out the buffer."""
- buf = Buffer(b"Foobar")
- data = buf.flush()
- assert data == b"Foobar"
- assert buf == bytearray()
-
-
-def test_remainig() -> None:
- """Buffer should report correct amount of remaining bytes to be read."""
- buf = Buffer(b"012345") # 6 bytes to be read
- assert buf.remaining == 6
- buf.read(2)
- assert buf.remaining == 4
- buf.clear()
- assert buf.remaining == 0
diff --git a/tests/test_component.py b/tests/test_component.py
deleted file mode 100644
index d2ce838..0000000
--- a/tests/test_component.py
+++ /dev/null
@@ -1,370 +0,0 @@
-from __future__ import annotations
-
-import gc
-
-import pytest
-import trio
-
-from azul.component import (
- Component,
- ComponentManager,
- Event,
- ExternalRaiseManager,
-)
-
-
-def test_event_init() -> None:
- event = Event("event_name", {"fish": 27}, 3)
- assert event.name == "event_name"
- assert event.data == {"fish": 27}
- assert event.level == 3
-
-
-def test_event_pop_level() -> None:
- event = Event("event_name", None, 3)
- assert event.pop_level()
- assert event.level == 2
- assert event.pop_level()
- assert event.level == 1
- assert event.pop_level()
- assert event.level == 0
-
- assert not event.pop_level()
- assert event.level == 0
-
-
-def test_event_repr() -> None:
- assert repr(Event("cat_moved", (3, 3))) == "Event('cat_moved', (3, 3), 0)"
-
-
-def test_component_init() -> None:
- component = Component("component_name")
- assert component.name == "component_name"
-
-
-def test_component_repr() -> None:
- assert repr(Component("fish")) == "Component('fish')"
-
-
-def test_component_manager_property_error() -> None:
- component = Component("waffle")
- assert not component.manager_exists
- with pytest.raises(
- AttributeError,
- match="^No component manager bound for",
- ):
- component.manager # noqa: B018
-
-
-def test_componentmanager_add_has_manager_property() -> None:
- manager = ComponentManager("manager")
- sound_effect = Component("sound_effect")
- with pytest.raises(AttributeError):
- print(sound_effect.manager)
- manager.add_component(sound_effect)
- assert manager.component_exists("sound_effect")
- assert sound_effect.manager_exists
- assert sound_effect.manager is manager
- assert sound_effect.component_exists("sound_effect")
- assert sound_effect.components_exist(("sound_effect",))
- assert not sound_effect.components_exist(("sound_effect", "waffle"))
- assert manager.list_components() == ("sound_effect",)
- assert sound_effect.get_component("sound_effect") is sound_effect
- assert sound_effect.get_components(("sound_effect",)) == [sound_effect]
-
-
-def test_componentmanager_manager_property_weakref_failure() -> None:
- # Have to override __del__, unbind_components called and unbinds
- # components so weakref failure branch never hit in normal
- # circumstances
- class EvilNoUnbindManager(ComponentManager):
- def __del__(self) -> None:
- return
-
- manager = EvilNoUnbindManager("manager")
- sound_effect = Component("sound_effect")
- with pytest.raises(AttributeError):
- print(sound_effect.manager)
- manager.add_component(sound_effect)
- assert sound_effect.manager is manager
- del manager
- # make sure gc collects manager
- for _ in range(3):
- gc.collect()
- with pytest.raises(AttributeError):
- print(sound_effect.manager)
-
-
-def test_double_bind_error() -> None:
- manager = ComponentManager("manager")
- sound_effect = Component("sound_effect")
- manager.add_component(sound_effect)
- manager_two = ComponentManager("manager_two")
- with pytest.raises(RuntimeError, match="component is already bound to"):
- manager_two.add_component(sound_effect)
-
-
-def test_self_component() -> None:
- manager = ComponentManager("manager", "cat_event")
- assert manager.component_exists("cat_event")
- assert manager.get_component("cat_event") is manager
-
- cat_event = Component("cat_event")
- with pytest.raises(ValueError, match="already exists"):
- manager.add_component(cat_event)
-
-
-def test_add_multiple() -> None:
- manager = ComponentManager("manager")
- manager.add_components(
- (
- Component("fish"),
- Component("waffle"),
- ),
- )
- assert manager.component_exists("fish")
- assert manager.component_exists("waffle")
-
- manager.unbind_components()
- assert not manager.get_all_components()
-
-
-def test_component_not_exist_error() -> None:
- manager = ComponentManager("manager")
- with pytest.raises(ValueError, match="does not exist"):
- manager.remove_component("darkness")
- with pytest.raises(ValueError, match="does not exist"):
- manager.get_component("darkness")
-
-
-@pytest.mark.trio
-async def test_self_component_handler() -> None:
- event_called = False
-
- async def event_call(event: Event[None]) -> None:
- nonlocal event_called
- assert event.name == "fish_appears_event"
- event_called = True
-
- manager = ComponentManager("manager", "cat")
- manager.register_handler("fish_appears_event", event_call)
-
- assert manager.has_handler("fish_appears_event")
-
- await manager.raise_event(Event("fish_appears_event", None))
- assert event_called
-
-
-@pytest.mark.trio
-async def test_raise_event_register_handlers_double_call() -> None:
- event_called_count = 0
-
- async def event_call(event: Event[int]) -> None:
- nonlocal event_called_count
- assert event.data == 27
- event_called_count += 1
-
- manager = ComponentManager("manager")
- assert not manager.has_handler("event_name")
-
- manager.register_component_handler("event_name", event_call, manager.name)
- assert manager.has_handler("event_name")
- await manager.raise_event(Event("event_name", 27))
- assert event_called_count == 1
-
- event_called_count = 0
-
- with pytest.raises(ValueError, match="is not registered!"):
- manager.register_component_handler(
- "event_name",
- event_call,
- "2nd name",
- )
- manager.add_component(Component("2nd name"))
- manager.register_component_handler("event_name", event_call, "2nd name")
-
- await manager.raise_event(Event("event_name", 27))
- assert event_called_count == 2
-
-
-@pytest.mark.trio
-async def test_raise_event_register_handlers() -> None:
- event_called = False
-
- async def event_call(event: Event[int]) -> None:
- nonlocal event_called
- assert event.data == 27
- event_called = True
-
- manager = ComponentManager("manager")
- sound_effect = Component("sound_effect")
- manager.add_component(sound_effect)
- sound_effect.register_handlers({"event_name": event_call})
-
- assert sound_effect.has_handler("event_name")
-
- await sound_effect.raise_event(Event("event_name", 27))
- assert event_called
-
- event_called = False
- await manager.raise_event(Event("event_name", 27))
- assert event_called
-
- event_called = False
- manager.remove_component("sound_effect")
- with pytest.raises(AttributeError, match="No component manager bound for"):
- await sound_effect.raise_event(Event("event_name", 27))
- await manager.raise_event(Event("event_name", 27))
- assert not event_called
-
-
-@pytest.mark.trio
-async def test_raise_leveled_comes_back() -> None:
- event_called = False
-
- async def event_call(event: Event[int]) -> None:
- nonlocal event_called
- assert event.level == 0
- event_called = True
-
- event_called_two = False
-
- async def event_call_two(event: Event[int]) -> None:
- nonlocal event_called_two
- assert event.level == 0
- event_called_two = True
-
- super_manager = ComponentManager("super_manager")
- manager = ComponentManager("manager")
-
- super_manager.add_component(manager)
- assert super_manager.component_exists("manager")
-
- super_manager.register_handler("leveled_event", event_call)
- manager.register_handler("leveled_event", event_call_two)
-
- await manager.raise_event(Event("leveled_event", None, 1))
- assert event_called
- assert event_called_two
-
-
-@pytest.mark.trio
-async def test_raise_event_register_handler() -> None:
- event_called = False
-
- async def event_call(event: Event[int]) -> None:
- nonlocal event_called
- assert event.data == 27
- event_called = True
-
- manager = ComponentManager("manager")
- sound_effect = Component("sound_effect")
- manager.add_component(sound_effect)
- sound_effect.register_handler("event_name", event_call)
-
- await sound_effect.raise_event(Event("event_name", 27))
- assert event_called
-
-
-@pytest.mark.trio
-async def test_raises_event_in_nursery() -> None:
- nursery_called = False
- event_called = False
-
- async def call_bean(event: Event[None]) -> None:
- nonlocal event_called
- assert event.name == "bean_event"
- event_called = True
-
- async with trio.open_nursery() as nursery:
- original = nursery.start_soon
-
- def replacement(*args: object, **kwargs: object) -> object:
- nonlocal nursery_called
- nursery_called = True
- return original(*args, **kwargs)
-
- nursery.start_soon = replacement
-
- manager = ExternalRaiseManager("manager", nursery)
- manager.register_handler("bean_event", call_bean)
- await manager.raise_event(Event("bean_event", None))
- assert nursery_called
- assert event_called
-
-
-@pytest.mark.trio
-async def test_internal_does_not_raise_event_in_nursery() -> None:
- nursery_called = False
- event_called = False
-
- async def call_bean(event: Event[None]) -> None:
- nonlocal event_called
- assert event.name == "bean_event"
- event_called = True
-
- async with trio.open_nursery() as nursery:
- original = nursery.start_soon
-
- def replacement(*args: object, **kwargs: object) -> object:
- nonlocal nursery_called
- nursery_called = True
- return original(*args, **kwargs)
-
- nursery.start_soon = replacement
-
- manager = ExternalRaiseManager("manager", nursery)
- manager.register_handler("bean_event", call_bean)
- await manager.raise_event_internal(Event("bean_event", None))
- assert not nursery_called
- assert event_called
-
-
-@pytest.mark.trio
-async def test_temporary_component() -> None:
- event_called = False
-
- async def event_call(event: Event[int]) -> None:
- nonlocal event_called
- assert event.data == 27
- event_called = True
-
- manager = ComponentManager("manager")
- with manager.temporary_component(
- Component("sound_effect"),
- ) as sound_effect:
- assert manager.component_exists("sound_effect")
- sound_effect.register_handler("event_name", event_call)
-
- await sound_effect.raise_event(Event("event_name", 27))
- assert event_called
- assert not manager.component_exists("sound_effect")
- with manager.temporary_component(
- Component("sound_effect"),
- ) as sound_effect:
- manager.remove_component("sound_effect")
-
-
-@pytest.mark.trio
-async def test_remove_component() -> None:
- event_called = False
-
- async def event_call(event: Event[int]) -> None:
- nonlocal event_called
- assert event.data == 27
- event_called = True
-
- manager = ComponentManager("manager")
- sound_effect = Component("sound_effect")
- manager.add_component(sound_effect)
- assert manager.component_exists("sound_effect")
- sound_effect.register_handler("event_name", event_call)
- sound_effect.register_handler("waffle_name", event_call)
-
- await sound_effect.raise_event(Event("event_name", 27))
- assert event_called
- manager.add_component(Component("jerald"))
- manager.register_handler("event_name", event_call)
- manager.remove_component("jerald")
- manager.remove_component("sound_effect")
- assert not manager.component_exists("sound_effect")
diff --git a/tests/test_encrypted_event.py b/tests/test_encrypted_event.py
deleted file mode 100644
index aaa837d..0000000
--- a/tests/test_encrypted_event.py
+++ /dev/null
@@ -1,79 +0,0 @@
-from __future__ import annotations
-
-import pytest
-import trio
-import trio.testing
-
-from azul.component import Event
-from azul.encrypted_event import EncryptedNetworkEventComponent
-
-
-@pytest.mark.trio
-async def test_event_transmission() -> None:
- one, two = trio.testing.memory_stream_pair()
- client_one = EncryptedNetworkEventComponent.from_stream("one", stream=one)
- client_two = EncryptedNetworkEventComponent.from_stream("two", stream=two)
-
- client_one.register_network_write_event("echo_event", 0)
- client_two.register_read_network_event(0, "reposted_event")
-
- event = Event(
- "echo_event",
- bytearray("I will give my cat food to bob", "utf-8"),
- 3,
- )
-
- await client_one.write_event(event)
- read_event = await client_two.read_event()
- assert read_event.name == "reposted_event"
- assert read_event.data == event.data
-
- await client_one.close()
- await client_two.close()
-
-
-@pytest.mark.trio
-async def test_event_encrypted_transmission() -> None:
- verification_token = bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8")
- shared_secret = bytes.fromhex(
- "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26",
- )
-
- one, two = trio.testing.memory_stream_pair()
- client_one = EncryptedNetworkEventComponent.from_stream("one", stream=one)
- client_two = EncryptedNetworkEventComponent.from_stream("two", stream=two)
-
- client_one.register_network_write_event("echo_event", 0)
- client_two.register_read_network_event(0, "reposted_event")
-
- event = Event(
- "echo_event",
- bytearray("I will give my cat food to bob", "utf-8"),
- 3,
- )
-
- await client_one.write_event(event)
- read_event = await client_two.read_event()
- assert read_event.name == "reposted_event"
- assert read_event.data == event.data
-
- await client_one.write_event(event)
- assert (
- await two.receive_some() == b"\x00\x1eI will give my cat food to bob"
- )
-
- client_one.enable_encryption(shared_secret, verification_token)
- client_two.enable_encryption(shared_secret, verification_token)
-
- await client_one.write_event(event)
- read_event = await client_two.read_event()
- assert read_event.name == "reposted_event"
- assert read_event.data == event.data
-
- await client_one.write_event(event)
- assert await two.receive_some() == bytearray.fromhex(
- "2bb572309dfb71d22eb5f0442c5347f2d666ed16c97093190a8101c3e59f2beb",
- )
-
- await client_one.close()
- await client_two.close()
diff --git a/tests/test_encryption.py b/tests/test_encryption.py
deleted file mode 100644
index 4f588c0..0000000
--- a/tests/test_encryption.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# This is the buffer module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-from typing import cast
-
-from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP
-from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
-from cryptography.hazmat.primitives.hashes import SHA256
-from cryptography.hazmat.primitives.serialization import load_pem_private_key
-
-from azul.encryption import (
- decrypt_token_and_secret,
- deserialize_public_key,
- encrypt_token_and_secret,
- serialize_public_key,
-)
-
-_SERIALIZED_RSA_PRIVATE_KEY = b"""
------BEGIN PRIVATE KEY-----
-MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMtRUQmRHqPkdA2K
-F6fM2c8ibIPHYV5KVQXNEkVx7iEKS6JsfELhX1H8t/qQ3Ob4Pr4OFjgXx9n7GvfZ
-gekNoswG6lnQH/n7t2sYA 6D+WvSix1FF2J6wPmpKriHS59TDk4opjaV14S4K4XjW
-Gmm8DqCzgXkPGC2dunFb+1A8mdkrAgMBAAECgYAWj2dWkGu989OMzQ3i6LAic8dm
-t/Dt7YGRqzejzQiHUgUieLcxFKDnEAu6GejpGBKeNCHzB3B9l4deiRwJKCIwHqMN
-LKMKoayinA8mj/Y/ O/ELDofkEyeXOhFyM642sPpaxQJoNWc9QEsYbxpG2zeB3sPf
-l3eIhkYTKVdxB+o8AQJBAPiddMjU8fuHyjKT6VCL2ZQbwnrRe1AaLLE6VLwEZuZC
-wlbx5Lcszi77PkMRTvltQW39VN6MEjiYFSPtRJleA+sCQQDRW2e3BX6uiil2IZ08
-tPFMnltFJpa 8YvW50N6mySd8Zg1oQJpzP2fC0n0+K4j3EiA/Zli8jBt45cJ4dMGX
-km/BAkEAtkYy5j+BvolbDGP3Ti+KcRU9K/DD+QGHvNRoZYTQsIdHlpk4t7eo3zci
-+ecJwMOCkhKHE7cccNPHxBRkFBGiywJAJBt2pMsu0R2FDxm3C6xNXaCGL0P7hVwv
-8y9B51 QUGlFjiJJz0OKjm6c/8IQDqFEY/LZDIamsZ0qBItNIPEMGQQJALZV0GD5Y
-zmnkw1hek/JcfQBlVYo3gFmWBh6Hl1Lb7p3TKUViJCA1k2f0aGv7+d9aFS0fRq6u
-/sETkem8Jc1s3g==
------END PRIVATE KEY-----
-"""
-RSA_PRIVATE_KEY = cast(
- RSAPrivateKey,
- load_pem_private_key(_SERIALIZED_RSA_PRIVATE_KEY, password=None),
-)
-RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key()
-SERIALIZED_RSA_PUBLIC_KEY = bytes.fromhex(
- "30819f300d06092a864886f70d010101050003818d0030818902818100cb515109911ea3e4740d8a17a7ccd9cf226c83c7615e4a5505cd124571ee210a4ba26c7c42e15f51fcb7fa90dce6f83ebe0e163817c7d9fb1af7d981e90da2cc06ea59d01ff9fbb76b1803a0fe5af4a2c75145d89eb03e6a4aae21d2e7d4c3938a298da575e12e0ae178d61a69bc0ea0b381790f182d9dba715bfb503c99d92b0203010001",
-)
-
-
-def test_encrypt_token_and_secret() -> None:
- """Test encryption returns properly encrypted (decryptable) values."""
- verification_token = bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8")
- shared_secret = bytes.fromhex(
- "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26",
- )
-
- encrypted_token, encrypted_secret = encrypt_token_and_secret(
- RSA_PUBLIC_KEY,
- verification_token,
- shared_secret,
- )
-
- assert (
- RSA_PRIVATE_KEY.decrypt(
- encrypted_token,
- OAEP(MGF1(SHA256()), SHA256(), None),
- )
- == verification_token
- )
- assert (
- RSA_PRIVATE_KEY.decrypt(
- encrypted_secret,
- OAEP(MGF1(SHA256()), SHA256(), None),
- )
- == shared_secret
- )
-
-
-def test_decrypt_token_and_secret() -> None:
- """Test decryption returns properly decrypted values."""
- encrypted_token = bytes.fromhex(
- "5541c0c0fc99d8908ed428b20c260795bec7b4041a4f98d26fbed383e8dba077eb53fb5cf905e722e2ceb341843e875508134817bcd3a909ac279e77ed94fd98c428bbe00db630a5ad3df310380d9274ed369cc6a011e7edd45cbe44ae8ad2575ef793b23057e4b15f1b6e3e195ff0921e46370773218517922fbb8b96092d88",
- )
- encrypted_secret = bytes.fromhex(
- "1a43782ca17f71e87e6ef98f9be66050ecf5d185da81445d26ceb5941f95d69d61b726d27b5ca62aed4cbe27b40fd4bd6b16b5be154a7b6a24ae31c705bc47d9397589b448fb72b14572ea2a9d843c6a3c674b7454cef97e2d65be36e0d0a8cc9f1093a19a8d52a5633a5317d19779bb46146dfaea7a690a7f080fb77d59c7f9",
- )
-
- assert decrypt_token_and_secret(
- RSA_PRIVATE_KEY,
- encrypted_token,
- encrypted_secret,
- ) == (
- bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8"),
- bytes.fromhex(
- "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26",
- ),
- )
-
-
-def test_serialize_public_key() -> None:
- """Test serialize_public_key."""
- assert serialize_public_key(RSA_PUBLIC_KEY) == SERIALIZED_RSA_PUBLIC_KEY
-
-
-def test_deserialize_public_key() -> None:
- """Test deserialize_public_key."""
- assert deserialize_public_key(SERIALIZED_RSA_PUBLIC_KEY) == RSA_PUBLIC_KEY
diff --git a/tests/test_network.py b/tests/test_network.py
deleted file mode 100644
index 0273bff..0000000
--- a/tests/test_network.py
+++ /dev/null
@@ -1,178 +0,0 @@
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
-
-import pytest
-import trio
-import trio.testing
-
-from azul.component import ComponentManager, Event
-from azul.network import (
- NetworkComponent,
- NetworkEventComponent,
- NetworkStreamNotConnectedError,
- NetworkTimeoutError,
- Server,
-)
-
-if TYPE_CHECKING:
- from collections.abc import Callable
-
-
-@pytest.mark.trio
-async def client_connect(port: int, stop_server: Callable[[], None]) -> None:
- await trio.sleep(0.05)
- # manager = ComponentManager("manager")
-
- client = NetworkEventComponent("client")
- # manager.add_component(client)
-
- await client.connect("127.0.0.1", port)
-
- client.register_network_write_event("echo_event", 0)
- client.register_read_network_event(1, "reposted_event")
-
- event = Event(
- "echo_event",
- bytearray("I will give my cat food to bob", "utf-8"),
- 3,
- )
-
- # await client.raise_event(event)
- await client.write_event(event)
- print(f"{await client.read_event() = }")
-
- await client.close()
- stop_server()
-
-
-@pytest.mark.trio
-async def run_async() -> None:
- class TestServer(Server):
- async def handler(self, stream: trio.SocketStream) -> None:
- client = NetworkEventComponent.from_stream("client", stream=stream)
-
- client.register_read_network_event(0, "repost_event")
- client.register_network_write_event("repost_event", 1)
-
- await client.write_event(await client.read_event())
- await stream.aclose()
-
- server = TestServer("server")
- port = 3004
- async with trio.open_nursery() as nursery:
- nursery.start_soon(server.serve, port)
- nursery.start_soon(client_connect, port, server.stop_serving)
- nursery.start_soon(client_connect, port, server.stop_serving)
-
-
-def test_not_connected() -> None:
- client = NetworkComponent("name")
- assert client.not_connected
- with pytest.raises(NetworkStreamNotConnectedError):
- print(client.stream)
-
-
-@pytest.mark.trio
-async def test_from_stream() -> None:
- stream = trio.testing.MemorySendStream()
-
- named = NetworkComponent.from_stream(
- kwargs={"name": "name"},
- stream=stream,
- )
- with pytest.raises(RuntimeError, match="Already connected!"):
- await named.connect("example.com", 80)
- await named.close()
-
-
-@pytest.mark.trio
-async def test_register_network_write_event() -> None:
- client = NetworkEventComponent("client")
- client.register_network_write_event("echo_event", 0)
- with pytest.raises(ValueError, match="event already registered"):
- client.register_network_write_event("echo_event", 0)
- client.register_read_network_event(0, "reposted_event")
- with pytest.raises(ValueError, match="events are also being received"):
- client.register_network_write_events({"reposted_event": 0})
- with pytest.raises(RuntimeError, match="Unhandled network event name"):
- await client.write_event(Event("jerald event", bytearray()))
- client.register_network_write_events({})
-
-
-@pytest.mark.trio
-async def test_register_network_read_event() -> None:
- one, two = trio.testing.memory_stream_pair()
- client_one = NetworkEventComponent.from_stream("one", stream=one)
- client_two = NetworkEventComponent.from_stream("two", stream=two)
- client_one.register_network_write_event("echo_event", 0)
- await client_one.write_event(
- Event(
- "echo_event",
- bytearray("I will give my cat food to bob", "utf-8"),
- ),
- )
- with pytest.raises(RuntimeError, match="Unhandled packet ID 0"):
- await client_two.read_event()
- with pytest.raises(ValueError, match="Packet id 0 packets are also"):
- client_one.register_read_network_event(0, "echo_event")
- client_two.register_read_network_event(0, "reposted_event")
- with pytest.raises(ValueError, match="Packet ID 0 already registered!"):
- client_two.register_read_network_events({0: "type_two"})
- client_two.register_read_network_events({})
-
-
-@pytest.mark.trio
-async def test_event_transmission() -> None:
- one, two = trio.testing.memory_stream_pair()
- client_one = NetworkEventComponent.from_stream("one", stream=one)
- manager = ComponentManager("manager")
- async with NetworkEventComponent.from_stream(
- "two",
- stream=two,
- ) as client_two:
- manager.add_component(client_one)
-
- assert not client_one.not_connected
-
- client_one.register_network_write_event("echo_event", 0)
- client_two.register_read_network_event(0, "reposted_event")
-
- event = Event(
- "echo_event",
- bytearray("I will give my cat food to bob", "utf-8"),
- 3,
- )
-
- await client_one.write_event(event)
- read_event = await client_two.read_event()
- assert read_event.name == "reposted_event"
- assert read_event.data == event.data
-
- await client_one.write_event(event)
- assert (
- await two.receive_some()
- == b"\x00\x1eI will give my cat food to bob"
- )
-
- await client_one.wait_write_might_not_block()
- await one.send_all(b"")
- client_two.timeout = 0.05
- with pytest.raises(NetworkTimeoutError):
- await client_two.read_event()
- await one.send_all(b"cat")
- with pytest.raises(OSError, match="Server stopped responding"):
- await client_two.read(4)
-
- await client_one.send_eof()
- await client_one.send_eof()
-
- await client_one.close()
- await client_one.close()
-
-
-def test_server() -> None:
- server = Server("name")
- server.stop_serving()
- server.serve_cancel_scope = trio.CancelScope()
- server.stop_serving()
diff --git a/tests/test_utils.py b/tests/test_utils.py
deleted file mode 100644
index 37b40b4..0000000
--- a/tests/test_utils.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-import pytest
-
-from azul.utils import from_twos_complement, to_twos_complement
-
-# TODO: Consider adding tests for enforce_range
-
-
-@pytest.mark.parametrize(
- ("number", "bits", "expected_out"),
- [
- (0, 8, 0),
- (1, 8, 1),
- (10, 8, 10),
- (127, 8, 127),
- ],
-)
-def test_to_twos_complement_positive(
- number: int,
- bits: int,
- expected_out: int,
-):
- """Test conversion to two's complement format from positive numbers gives expected result."""
- assert to_twos_complement(number, bits) == expected_out
-
-
-@pytest.mark.parametrize(
- ("number", "bits", "expected_out"),
- [
- (-1, 8, 255),
- (-10, 8, 246),
- (-128, 8, 128),
- ],
-)
-def test_to_twos_complement_negative(
- number: int,
- bits: int,
- expected_out: int,
-):
- """Test conversion to two's complement format of negative numbers gives expected result."""
- assert to_twos_complement(number, bits) == expected_out
-
-
-@pytest.mark.parametrize(
- ("number", "bits"),
- [
- (128, 8),
- (-129, 8),
- (32768, 16),
- (-32769, 16),
- (2147483648, 32),
- (-2147483649, 32),
- (9223372036854775808, 64),
- (-9223372036854775809, 64),
- ],
-)
-def test_to_twos_complement_range(number: int, bits: int):
- """Test conversion to two's complement format for out of range numbers raises :exc:`ValueError`."""
- with pytest.raises(ValueError, match="out of range"):
- to_twos_complement(number, bits)
-
-
-@pytest.mark.parametrize(
- ("number", "bits", "expected_out"),
- [
- (0, 8, 0),
- (1, 8, 1),
- (10, 8, 10),
- (127, 8, 127),
- ],
-)
-def test_from_twos_complement_positive(
- number: int,
- bits: int,
- expected_out: int,
-):
- """Test conversion from two's complement format of positive numbers give expected result."""
- assert from_twos_complement(number, bits) == expected_out
-
-
-@pytest.mark.parametrize(
- ("number", "bits", "expected_out"),
- [
- (255, 8, -1),
- (246, 8, -10),
- (128, 8, -128),
- ],
-)
-def test_from_twos_complement_negative(
- number: int,
- bits: int,
- expected_out: int,
-):
- """Test conversion from two's complement format of negative numbers give expected result."""
- assert from_twos_complement(number, bits) == expected_out
-
-
-@pytest.mark.parametrize(
- ("number", "bits"),
- [
- (256, 8),
- (-1, 8),
- (65536, 16),
- (-1, 16),
- (4294967296, 32),
- (-1, 32),
- (18446744073709551616, 64),
- (-1, 64),
- ],
-)
-def test_from_twos_complement_range(number: int, bits: int):
- """Test conversion from two's complement format for out of range numbers raises :exc:`ValueError`."""
- with pytest.raises(ValueError, match="out of range"):
- from_twos_complement(number, bits)
From 86f4d2bf6225d39b1b10a605645d6ebbfaf645b0 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Mon, 25 Nov 2024 12:49:51 -0600
Subject: [PATCH 05/67] Add more tests from catto invasion and upgrade
gitignore
---
.gitignore | 28 ++++-
pyproject.toml | 6 +
src/azul/sprite.py | 5 +-
test-requirements.in | 2 +-
tests/test_async_clock.py | 49 ++++++--
tests/test_objects.py | 81 ++++++++++++
tests/test_sprite.py | 255 ++++++++++++++++++++++++++++++++++++++
7 files changed, 411 insertions(+), 15 deletions(-)
create mode 100644 tests/test_objects.py
create mode 100644 tests/test_sprite.py
diff --git a/.gitignore b/.gitignore
index 8d514aa..a55ab68 100644
--- a/.gitignore
+++ b/.gitignore
@@ -86,6 +86,7 @@ instance/
docs/_build/
# PyBuilder
+.pybuilder/
target/
# Jupyter Notebook
@@ -96,6 +97,8 @@ profile_default/
ipython_config.py
# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
.python-version
# pipenv
@@ -105,6 +108,21 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
@@ -139,11 +157,15 @@ venv.bak/
.dmypy.json
dmypy.json
-# Pyre type checker
+# Pyre static type analyzer
.pyre/
-# Sphinx documentation
-doc/_build/
+# Cython debug symbols
+cython_debug/
# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
diff --git a/pyproject.toml b/pyproject.toml
index b8a5cce..53b856d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -62,10 +62,16 @@ azul = ["py.typed", "data/*"]
plugins = ["numpy.typing.mypy_plugin"]
files = ["src/azul/",]
check_untyped_defs = true
+disallow_any_decorated = true
disallow_any_generics = true
+disallow_any_unimported = true
+disallow_incomplete_defs = true
+disallow_subclassing_any = true
disallow_untyped_calls = true
+disallow_untyped_decorators = true
disallow_untyped_defs = true
ignore_missing_imports = true
+local_partial_types = true
no_implicit_optional = true
no_implicit_reexport = true
show_column_numbers = true
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index fbd1915..4cff67c 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -693,13 +693,12 @@ class GroupProcessor(AsyncStateMachine):
__slots__ = ("_clear", "_timing", "group_names", "groups", "new_gid")
sub_renderer_class: ClassVar = LayeredDirty
- groups: dict[int, sub_renderer_class]
def __init__(self) -> None:
"""Initialize group processor."""
super().__init__()
- self.groups = {}
+ self.groups: dict[int, LayeredDirty[Sprite]] = {}
self.group_names: dict[str, int] = {}
self.new_gid = 0
self._timing = 1000 / 80
@@ -751,7 +750,7 @@ def remove_group(self, gid: int) -> None:
del self.group_names[name]
return
- def get_group(self, gid_name: str | int) -> sub_renderer_class | None:
+ def get_group(self, gid_name: str | int) -> LayeredDirty[Sprite] | None:
"""Return group from group ID or name."""
named = None
if isinstance(gid_name, str):
diff --git a/test-requirements.in b/test-requirements.in
index 6c3b0de..0094b39 100644
--- a/test-requirements.in
+++ b/test-requirements.in
@@ -1,5 +1,5 @@
# For tests
-pytest >= 5.0 # for faulthandler in core
+pytest >= 5.0
coverage >= 7.2.5
pytest-trio
pytest-cov
diff --git a/tests/test_async_clock.py b/tests/test_async_clock.py
index aac40bd..7c97954 100644
--- a/tests/test_async_clock.py
+++ b/tests/test_async_clock.py
@@ -1,27 +1,60 @@
+from __future__ import annotations
+
import pytest
-from azul import async_clock
+from azul.async_clock import Clock
+
+
+@pytest.fixture
+def clock() -> Clock:
+ return Clock()
+
+
+def test_initial_values(clock: Clock) -> None:
+ assert clock.fps == 0.0
+ assert clock.fps_count == 0
+
+
+def test_get_fps(clock: Clock) -> None:
+ assert clock.get_fps() == 0.0
+
+
+def test_get_rawtime(clock: Clock) -> None:
+ assert clock.get_rawtime() == 0
+
+
+def test_get_time(clock: Clock) -> None:
+ assert clock.get_time() == 0
@pytest.mark.trio
-async def test_tick() -> None:
- clock = async_clock.Clock()
+async def test_tick_elasped(clock: Clock) -> None:
+ time_passed = await clock.tick()
+ assert time_passed >= 0
+
+ # Test with a specific framerate
+ time_passed = await clock.tick(60)
+ assert time_passed >= int(1e9 // 60)
+ # Test with a zero framerate
+ time_passed = await clock.tick(0)
+ assert time_passed >= 0
+
+
+@pytest.mark.trio
+async def test_tick(clock: Clock) -> None:
await clock.tick(60)
result = await clock.tick(60)
assert isinstance(result, int)
- assert result >= 0
+ assert result >= int(1e9 // 60)
assert repr(clock).startswith(" None:
- clock = async_clock.Clock()
-
+async def test_tick_fps(clock: Clock) -> None:
for _ in range(20):
await clock.tick(60)
fps = clock.get_fps()
diff --git a/tests/test_objects.py b/tests/test_objects.py
new file mode 100644
index 0000000..12e2b56
--- /dev/null
+++ b/tests/test_objects.py
@@ -0,0 +1,81 @@
+from __future__ import annotations
+
+import pytest
+from pygame.surface import Surface
+
+from azul.objects import Button, OutlinedText, Text
+
+
+class MockSurface(Surface):
+ """Mocking a pygame surface for testing."""
+
+ __slots__ = ("text_data",)
+
+ def __init__(self, text_data: str = "") -> None:
+ super().__init__((0, 0))
+ self.text_data = text_data
+
+
+class MockFont:
+ """Mocking a pygame font for testing."""
+
+ __slots__ = ()
+
+ def render(
+ self,
+ text: str,
+ antialias: bool,
+ color: tuple[int, int, int],
+ ) -> str:
+ """Fake render method."""
+ return MockSurface(text)
+
+
+@pytest.fixture
+def font() -> MockFont:
+ return MockFont()
+
+
+def test_text_initialization(font: MockFont) -> None:
+ text = Text("TestText", font)
+ assert text.text == "None"
+ assert text.color == (255, 255, 255)
+ assert text.font == font
+
+
+def test_text_rendering(font: MockFont) -> None:
+ text = Text("TestText", font)
+ assert text.image is None
+
+
+def test_text_rendering_blank(font: MockFont) -> None:
+ text = Text("TestText", font)
+ text.text = ""
+ text.text = ""
+ assert text.image.text_data == ""
+
+
+def test_outlined_text_initialization(font: MockFont) -> None:
+ outlined_text = OutlinedText("TestOutlinedText", font)
+ assert outlined_text.outline == (0, 0, 0)
+ assert outlined_text.inside == (255, 255, 255)
+
+
+def test_outlined_text_rendering(font: MockFont) -> None:
+ outlined_text = OutlinedText("TestOutlinedText", font)
+ outlined_text.text = "Outlined Text"
+ assert outlined_text.text == "Outlined Text"
+
+
+def test_outlined_text_rendering_zero_border(font: MockFont) -> None:
+ outlined_text = OutlinedText("TestOutlinedText", font)
+ outlined_text.border_width = 0
+ outlined_text.text = "Outlined Text"
+ assert isinstance(outlined_text.image, Surface)
+
+
+def test_button_initialization(font: MockFont) -> None:
+ button = Button("TestButton", font)
+ assert button.text == "None"
+ assert button.color == (0, 0, 0, 255)
+ assert button.border_width == 3
diff --git a/tests/test_sprite.py b/tests/test_sprite.py
new file mode 100644
index 0000000..f2bda6c
--- /dev/null
+++ b/tests/test_sprite.py
@@ -0,0 +1,255 @@
+from __future__ import annotations
+
+import pytest
+import trio
+from libcomponent.component import Event
+from pygame.rect import Rect
+from pygame.surface import Surface
+
+from azul.sprite import (
+ AnimationComponent,
+ DragClickEventComponent,
+ GroupProcessor,
+ ImageComponent,
+ MovementComponent,
+ OutlineComponent,
+ Sprite,
+ TargetingComponent,
+ TickEventData,
+)
+from azul.vector import Vector2
+
+
+@pytest.fixture
+def sprite() -> Sprite:
+ return Sprite("test_sprite")
+
+
+@pytest.fixture
+def image_component(sprite: Sprite) -> ImageComponent:
+ sprite.add_component(ImageComponent())
+ return sprite.get_component("image")
+
+
+@pytest.fixture
+def animation_component(image_component: ImageComponent) -> AnimationComponent:
+ return image_component.get_component("animation")
+
+
+@pytest.fixture
+def outline_component(image_component: ImageComponent) -> OutlineComponent:
+ return image_component.get_component("outline")
+
+
+@pytest.fixture
+def movement_component(sprite: Sprite) -> MovementComponent:
+ sprite.add_component(MovementComponent())
+ return sprite.get_component("movement")
+
+
+@pytest.fixture
+def targeting_component(
+ movement_component: MovementComponent,
+) -> TargetingComponent:
+ sprite = movement_component.manager
+ sprite.add_component(TargetingComponent())
+ return sprite.get_component("targeting")
+
+
+@pytest.fixture
+def drag_click_event_component() -> DragClickEventComponent:
+ return DragClickEventComponent()
+
+
+@pytest.fixture
+def group_processor() -> GroupProcessor:
+ return GroupProcessor()
+
+
+def test_sprite_init(sprite: Sprite) -> None:
+ assert sprite.name == "test_sprite"
+ assert not sprite.visible
+ assert sprite.rect == Rect(0, 0, 0, 0)
+
+
+def test_sprite_location(sprite: Sprite) -> None:
+ sprite.location = (10, 20)
+ assert sprite.rect.center == (10, 20)
+
+
+def test_sprite_repr(sprite: Sprite) -> None:
+ assert repr(sprite) == ""
+
+
+def test_sprite_image(sprite: Sprite) -> None:
+ sprite.dirty = 0
+ assert sprite.image is None
+ assert not sprite.dirty
+ sprite.image = Surface((10, 10))
+ assert isinstance(sprite.image, Surface)
+ assert sprite.dirty
+ assert sprite.rect.size == (10, 10)
+
+
+def test_sprite_image_set_none(sprite: Sprite) -> None:
+ sprite.dirty = 0
+ assert sprite.image is None
+ assert not sprite.dirty
+ sprite.image = None
+ assert sprite.dirty
+
+
+def test_sprite_image_no_set_location_change(sprite: Sprite) -> None:
+ sprite.update_location_on_resize = False
+ sprite.location = (100, 100)
+ sprite.image = Surface((50, 25))
+ assert sprite.location == (125, 112)
+
+
+def test_sprite_image_set_location_change(sprite: Sprite) -> None:
+ sprite.update_location_on_resize = True
+ sprite.location = (100, 100)
+ sprite.image = Surface((50, 25))
+ assert sprite.location == (100, 100)
+
+
+def test_image_component_init(image_component: ImageComponent) -> None:
+ assert image_component.mask_threshold == 127
+
+
+def test_image_component_add_image(image_component: ImageComponent) -> None:
+ image = Surface((10, 10))
+ image_component.add_image("test_image", image)
+ assert "test_image" in image_component.list_images()
+
+
+def test_image_component_add_image_and_mask_invalid_image(
+ image_component: ImageComponent,
+) -> None:
+ with pytest.raises(
+ ValueError,
+ match="^Expected surface to be a valid identifier$",
+ ):
+ image_component.add_image_and_mask("test_image", None, None) # type: ignore[arg-type]
+ with pytest.raises(
+ ValueError,
+ match="^Expected surface to be a valid identifier$",
+ ):
+ image_component.add_image_and_mask("test_image", "copy_from", None) # type: ignore[arg-type]
+
+
+def test_image_component_add_image_and_mask_invalid_mask(
+ image_component: ImageComponent,
+) -> None:
+ image = Surface((1, 1))
+ with pytest.raises(
+ ValueError,
+ match="^Expected mask to be a valid identifier$",
+ ):
+ image_component.add_image_and_mask("test_image", image, None) # type: ignore[arg-type]
+ with pytest.raises(
+ ValueError,
+ match="^Expected mask to be a valid identifier$",
+ ):
+ image_component.add_image_and_mask("test_image", image, "copy_from") # type: ignore[arg-type]
+
+
+def test_image_component_get_image(image_component: ImageComponent) -> None:
+ image = Surface((1, 1))
+ image_component.add_image("test_image", image)
+ assert image_component.get_image("test_image") is image
+
+
+def test_image_component_add_image_duplication(
+ image_component: ImageComponent,
+) -> None:
+ image = Surface((1, 1))
+ image_component.add_image("test_image", image)
+ image_component.add_image("duplicate", "test_image")
+ assert image_component.get_image("duplicate") is image
+
+
+def test_movement_component_init(
+ movement_component: MovementComponent,
+) -> None:
+ assert movement_component.heading == Vector2(0, 0)
+ assert movement_component.speed == 0
+
+
+def test_movement_component_point_toward(
+ movement_component: MovementComponent,
+) -> None:
+ movement_component.point_toward((10, 20))
+ assert (
+ movement_component.heading
+ == Vector2.from_points((0, 0), (10, 20)).normalized()
+ )
+
+
+def test_movement_component_move_heading_time(
+ movement_component: MovementComponent,
+) -> None:
+ movement_component.speed = 5
+ movement_component.move_heading_time(1)
+ assert movement_component.heading * 5 == movement_component.heading
+
+
+def test_targeting_component_init(
+ targeting_component: TargetingComponent,
+) -> None:
+ assert targeting_component.destination == Vector2(0, 0)
+ assert targeting_component.event_raise_name == "reached_destination"
+
+
+def test_targeting_component_update_heading(
+ targeting_component: TargetingComponent,
+) -> None:
+ targeting_component.destination = Vector2(10, 20)
+ targeting_component.update_heading()
+ assert targeting_component.to_destination() == Vector2.from_points(
+ (0, 0),
+ (10, 20),
+ )
+
+
+@pytest.mark.trio
+async def test_targeting_component_move_destination_time(
+ targeting_component: TargetingComponent,
+) -> None:
+ movement_component = targeting_component.get_component("movement")
+ movement_component.speed = 1
+ targeting_component.destination = Vector2(10, 20)
+ current_distance = targeting_component.to_destination().magnitude()
+ await targeting_component.move_destination_time(1)
+ assert targeting_component.to_destination().magnitude() < current_distance
+
+
+def test_drag_click_event_component_init(
+ drag_click_event_component: DragClickEventComponent,
+) -> None:
+ assert drag_click_event_component.pressed == {}
+
+
+def test_group_processor_init(group_processor: GroupProcessor) -> None:
+ assert group_processor.groups == {}
+ assert group_processor.group_names == {}
+ assert group_processor.new_gid == 0
+
+
+def test_group_processor_new_group(group_processor: GroupProcessor) -> None:
+ gid = group_processor.new_group("test_group")
+ assert gid in group_processor.groups
+ assert "test_group" in group_processor.group_names
+
+
+@pytest.mark.trio
+async def test_animation_component_tick(
+ animation_component: AnimationComponent,
+) -> None:
+ async with trio.open_nursery() as nursery:
+ nursery.start_soon(
+ animation_component.tick,
+ Event("tick", TickEventData(time_passed=1, fps=60)),
+ )
+ await trio.lowlevel.checkpoint()
+ # Assert that the animation component has updated correctly
From bd061ab5a4eb64506491959f19422ad8eaa2f2e6 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Mon, 25 Nov 2024 23:15:45 -0600
Subject: [PATCH 06/67] Work on manual wall tiling and start server side work
---
src/azul/errorbox.py | 2 +-
src/azul/game.py | 4 +-
src/azul/network_shared.py | 41 ++++---
src/azul/server.py | 45 +++++---
src/azul/state.py | 211 +++++++++++++++++++++++++++++++++++--
5 files changed, 258 insertions(+), 45 deletions(-)
diff --git a/src/azul/errorbox.py b/src/azul/errorbox.py
index 6c6b3e4..3fcb40c 100644
--- a/src/azul/errorbox.py
+++ b/src/azul/errorbox.py
@@ -52,7 +52,7 @@ def __wxpython(title: str, message: str) -> None:
"""Error with wxPython."""
from wxPython.wx import wxApp, wxICON_EXCLAMATION, wxMessageDialog, wxOK
- class LameApp(wxApp): # type: ignore[misc]
+ class LameApp(wxApp): # type: ignore[misc,no-any-unimported]
__slots__ = ()
def OnInit(self) -> int: # noqa: N802
diff --git a/src/azul/game.py b/src/azul/game.py
index db25d88..db0bc45 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -2516,9 +2516,9 @@ def __init__(self) -> None:
self.host_mode = True
self.variant_play = False
- def entry_actions(self) -> None:
+ async def entry_actions(self) -> None:
"""Add cursor object and tons of button and text objects to the game."""
- super().entry_actions()
+ await super().entry_actions()
def add_numbers(
start: int,
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index aa30f5d..862ff9f 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -24,9 +24,12 @@
__license__ = "GNU General Public License Version 3"
+from collections import Counter
from enum import IntEnum, auto
-from typing import Final, NamedTuple, TypeAlias
+from typing import Final, TypeAlias
+from libcomponent.base_io import StructFormat
+from libcomponent.buffer import Buffer
from mypy_extensions import u8
ADVERTISEMENT_IP: Final = "224.0.2.60"
@@ -37,11 +40,28 @@
Pos: TypeAlias = tuple[u8, u8]
-class TickEventData(NamedTuple):
- """Tick Event Data."""
+def encode_numeric_uint8_counter(counter: Counter[int]) -> Buffer:
+ """Return buffer from uint8 counter (both keys and values)."""
+ buffer = Buffer()
- time_passed: float
- fps: float
+ for key, value in counter.items():
+ assert isinstance(key, int)
+ buffer.write_value(StructFormat.UBYTE, key)
+ buffer.write_value(StructFormat.UBYTE, value)
+ return buffer
+
+
+def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[int]:
+ """Return buffer from uint8 counter (both keys and values)."""
+ data: dict[int, int] = {}
+
+ for _ in range(0, len(buffer), 2):
+ key = buffer.read_value(StructFormat.UBYTE)
+ value = buffer.read_value(StructFormat.UBYTE)
+ assert key not in data
+ data[key] = value
+
+ return Counter(data)
class ClientBoundEvents(IntEnum):
@@ -51,15 +71,6 @@ class ClientBoundEvents(IntEnum):
callback_ping = auto()
initial_config = auto()
playing_as = auto()
- create_piece = auto()
- select_piece = auto()
- create_tile = auto()
- delete_tile = auto()
- animation_state = auto()
- delete_piece_animation = auto()
- update_piece_animation = auto()
- move_piece_animation = auto()
- action_complete = auto()
game_over = auto()
@@ -67,5 +78,3 @@ class ServerBoundEvents(IntEnum):
"""Server bound event IDs."""
encryption_response = 0
- select_piece = auto()
- select_tile = auto()
diff --git a/src/azul/server.py b/src/azul/server.py
index f361f62..4cd2226 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -54,7 +54,7 @@
ClientBoundEvents,
ServerBoundEvents,
)
-from azul.state import State
+from azul.state import Phase, State
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
@@ -196,7 +196,6 @@ class GameServer(network.Server):
"client_count",
"client_players",
"internal_singleplayer_mode",
- "player_selections",
"players_can_interact",
"running",
"state",
@@ -208,7 +207,7 @@ def __init__(self, internal_singleplayer_mode: bool = False) -> None:
"""Initialize server."""
super().__init__("GameServer")
- self.client_count: int
+ self.client_count: int = 0
self.state = State.new_game(0)
self.client_players: dict[int, int] = {}
@@ -323,18 +322,20 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]:
"""Return teams given sorted client ids."""
players: dict[int, int] = {}
for idx, client_id in enumerate(client_ids):
- if idx < 2:
- players[client_id] = idx % 2
+ if idx < 4:
+ players[client_id] = idx % 4
else:
players[client_id] = 0xFF # Spectator
return players
- def new_game_init(self) -> None:
+ def new_game_init(self, varient_play: bool = False) -> None:
"""Start new game."""
self.client_players.clear()
- ## pieces = generate_pieces(*self.board_size)
- self.state = State.new_game(self.client_count)
+ self.state = State.new_game(
+ max(2, min(4, self.client_count)),
+ varient_play,
+ )
# Why keep track of another object just to know client ID numbers
# if we already have that with the components? No need!
@@ -360,7 +361,6 @@ async def start_server( # type: ignore[misc]
print(f"{self.__class__.__name__}: Closing old server clients")
await self.stop_server()
print(f"{self.__class__.__name__}: Starting Server")
- self.client_count = 0
host, port = event.data
@@ -381,8 +381,9 @@ async def transmit_playing_as(self) -> None:
Event(f"playing_as->network[{client_id}]", team),
)
- async def handle_server_start_new_game(self, event: Event[None]) -> None:
+ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
"""Handle game start."""
+ varient_play = event.data
## # Delete all pieces from last state (shouldn't be needed but still.)
## async with trio.open_nursery() as nursery:
## for piece_pos, _piece_type in self.state.get_pieces():
@@ -393,7 +394,7 @@ async def handle_server_start_new_game(self, event: Event[None]) -> None:
# Choose which team plays first
# Using non-cryptographically secure random because it doesn't matter
- self.new_game_init()
+ self.new_game_init(varient_play)
## # Send create_piece events for all pieces
## async with trio.open_nursery() as nursery:
@@ -413,7 +414,11 @@ async def handle_server_start_new_game(self, event: Event[None]) -> None:
),
)
- async def client_network_loop(self, client: ServerClient) -> None:
+ async def client_network_loop(
+ self,
+ client: ServerClient,
+ controls_lobby: bool = False,
+ ) -> None:
"""Network loop for given ServerClient.
Could raise the following exceptions:
@@ -457,6 +462,7 @@ async def client_network_loop(self, client: ServerClient) -> None:
traceback.print_exception(exc)
break
if event is not None:
+ # if controls_lobby:
# print(f"{client.name} client_network_loop tick")
# print(f"{client.name} {event = }")
await client.raise_event(event)
@@ -469,7 +475,7 @@ def can_start(self) -> bool:
def game_active(self) -> bool:
"""Return if game is active."""
- return self.state.check_for_win() is None
+ return self.state.current_phase != Phase.end
async def send_spectator_join_packets(
self,
@@ -487,7 +493,7 @@ async def send_spectator_join_packets(
await client.raise_event(
Event(
"initial_config->network",
- (None, self.state.turn),
+ (self.state.varient_play, self.state.current_turn),
),
)
@@ -501,6 +507,10 @@ async def handler(self, stream: trio.SocketStream) -> None:
# Old game was running but everyone left, restart
print("TODO: restart")
new_client_id = self.client_count
+
+ # Is controlling player?
+ is_zee_capitan = new_client_id == 0
+
print(
f"{self.__class__.__name__}: client connected [client_id {new_client_id}]",
)
@@ -530,12 +540,13 @@ async def handler(self, stream: trio.SocketStream) -> None:
if can_start and game_active:
await self.send_spectator_join_packets(client)
with self.temporary_component(client):
- if can_start and not game_active:
+ if can_start and not game_active and is_zee_capitan:
+ varient_play = False
await self.raise_event(
- Event("server_send_game_start", None),
+ Event("server_send_game_start", varient_play),
)
try:
- await self.client_network_loop(client)
+ await self.client_network_loop(client, is_zee_capitan)
finally:
print(
f"{self.__class__.__name__}: client disconnected [client_id {new_client_id}]",
diff --git a/src/azul/state.py b/src/azul/state.py
index 5c836dc..7439967 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -181,6 +181,16 @@ def floor_fill_tile_excess(
return excess
+class UnplacableTileError(Exception):
+ """Unplacable Tile Exception."""
+
+ __slots__ = ("y",)
+
+ def __init__(self, y: int) -> None:
+ """Remember Y position."""
+ self.y = y
+
+
class PlayerData(NamedTuple):
"""Player data."""
@@ -392,9 +402,10 @@ def perform_auto_wall_tiling(self) -> tuple[Self, Counter[int], bool]:
for line_id, line in enumerate(self.lines):
if line.count_ != self.get_line_max_count(line_id):
continue
- right = max(0, line.count_ - 1)
- if right:
- for_box_lid[line.color] += right
+ left = max(0, line.count_ - 1)
+ if left:
+ for_box_lid[line.color] += left
+ # placed tile is stuck in the wall now
x = tuple(map(int, new_wall[line_id, :])).index(-line.color - 1)
score += self.get_score_from_wall_placement(
line.color,
@@ -462,6 +473,122 @@ def perform_end_of_game_scoring(self) -> Self:
"""Return new player data after performing end of game scoring."""
return self._replace(score=self.get_end_of_game_score())
+ def get_manual_wall_tile_location(self) -> tuple[int, list[int]] | None:
+ """Return tuple of row and placable columns for wall tiling, or None if done.
+
+ Raises UnplacableTileError if no valid placement locations.
+ """
+ for y, line in enumerate(self.lines):
+ if line.color == Tile.blank:
+ continue
+ if line.count_ != self.get_line_max_count(y):
+ continue
+
+ valid_x: list[int] = []
+ for x, is_open in enumerate(self.wall[y, :] >= 0):
+ if not is_open:
+ continue
+ if line.color in {Tile(int(v)) for v in self.wall[:, x]}:
+ continue
+ valid_x.append(x)
+ if not valid_x:
+ raise UnplacableTileError(y)
+ return (y, valid_x)
+ return None
+
+ def handle_unplacable_wall_tiling(
+ self,
+ y: int,
+ ) -> tuple[Self, Counter[int]]:
+ """Return new player data and tiles for floor line."""
+ line = self.lines[y]
+ assert line.color != Tile.blank
+
+ new_lines = self.replace_pattern_line(
+ self.lines,
+ y,
+ PatternLine.blank(),
+ )
+
+ return self._replace(
+ lines=new_lines,
+ ).place_floor_line_tiles(line.color, line.count_)
+
+ def manual_wall_tiling_action(
+ self,
+ line_id: int,
+ x_pos: int,
+ ) -> tuple[Self, Counter[int]]:
+ """Wall tile given full line to given x position in that row.
+
+ Return new player data and any tiles to return to box lid.
+ """
+ for_box_lid: Counter[int] = Counter()
+
+ score = self.score
+ new_lines = self.lines
+ new_wall = self.wall.copy()
+
+ line = self.lines[line_id]
+
+ assert line.count_ == self.get_line_max_count(line_id)
+ assert line.color != Tile.blank
+ assert new_wall[line_id, x_pos] == Tile.blank
+
+ left = max(0, line.count_ - 1)
+ if left:
+ for_box_lid[line.color] += left
+ # placed tile is stuck in wall now
+ score += self.get_score_from_wall_placement(
+ line.color,
+ x_pos,
+ line_id,
+ new_wall,
+ )
+ new_wall[line_id, x_pos] = line.color
+ new_lines = self.replace_pattern_line(
+ new_lines,
+ line_id,
+ PatternLine.blank(),
+ )
+
+ return (
+ self._replace(
+ lines=new_lines,
+ wall=new_wall,
+ score=score,
+ ),
+ for_box_lid,
+ )
+
+ def finish_manual_wall_tiling(self) -> tuple[Self, Counter[int], bool]:
+ """Return new player data and tiles for box lid after performing automatic wall tiling."""
+ for_box_lid: Counter[int] = Counter()
+
+ score = self.score
+
+ score += self.get_floor_line_scoring()
+ if score < 0:
+ score = 0
+
+ # Get one tile from floor line
+ floor = self.floor.copy()
+ has_one = False
+ if floor[Tile.one]:
+ floor[Tile.one] -= 1
+ remove_counter_zeros(floor)
+ has_one = True
+ for_box_lid.update(floor)
+
+ return (
+ self._replace(
+ score=score,
+ floor=Counter(),
+ ),
+ for_box_lid,
+ has_one,
+ )
+
def factory_displays_deepcopy(
factory_displays: dict[int, Counter[int]],
@@ -717,9 +844,7 @@ def _factory_offer_maybe_next_turn(self) -> Self:
current_phase=current_phase,
current_turn=current_turn,
)
- if current_phase == Phase.wall_tiling:
- if self.varient_play:
- return new_state.start_manual_wall_tiling()
+ if current_phase == Phase.wall_tiling and not self.varient_play:
return new_state.apply_auto_wall_tiling()
return new_state
@@ -1022,17 +1147,85 @@ def preform_action(
return new
raise NotImplementedError()
- def start_manual_wall_tiling(self) -> Self:
- """Return new state after starting manual wall tiling."""
+ def _manual_wall_tiling_maybe_next_turn(self) -> Self:
raise NotImplementedError()
return self
+ def get_manual_wall_tiling_locations_for_player(
+ self,
+ player_id: int,
+ ) -> tuple[int, list[int]] | None | Self:
+ """Either return player wall tiling location data or new state.
+
+ New state when player cannot wall tile their current row.
+ """
+ current_player_data = self.player_data[player_id]
+
+ try:
+ return current_player_data.get_manual_wall_tile_location()
+ except UnplacableTileError as unplacable_exc:
+ # kind of hacky, but it works
+ y_position = unplacable_exc.y
+
+ new_player_data, for_box_lid = (
+ current_player_data.handle_unplacable_wall_tiling(y_position)
+ )
+
+ box_lid = self.box_lid.copy()
+
+ # Add overflow tiles to box lid
+ assert all(x > 0 for x in for_box_lid.values()), for_box_lid
+ box_lid.update(for_box_lid)
+
+ # Update player data
+ player_data = player_data_deepcopy(self.player_data)
+ player_data[player_id] = new_player_data
+
+ return self._replace(
+ box_lid=box_lid,
+ player_data=player_data,
+ )._manual_wall_tiling_maybe_next_turn()
+
+ def manual_wall_tiling_action(
+ self,
+ player_id: int,
+ line_id: int,
+ x_pos: int,
+ ) -> Self:
+ """Perform manual wall tiling action."""
+ current_player_data = self.player_data[player_id]
+
+ new_player_data, for_box_lid = (
+ current_player_data.manual_wall_tiling_action(line_id, x_pos)
+ )
+ box_lid = self.box_lid.copy()
+
+ # Add overflow tiles to box lid
+ assert all(x > 0 for x in for_box_lid.values()), for_box_lid
+ box_lid.update(for_box_lid)
+
+ # Update player data
+ player_data = player_data_deepcopy(self.player_data)
+ player_data[player_id] = new_player_data
+
+ new_state = self._replace(
+ box_lid=box_lid,
+ player_data=player_data,
+ )
+
+ result = new_state.get_manual_wall_tiling_locations_for_player(
+ player_id,
+ )
+ if isinstance(result, tuple) or result is None:
+ return new_state._manual_wall_tiling_maybe_next_turn()
+ return result._manual_wall_tiling_maybe_next_turn()
+
def run() -> None:
"""Run program."""
from market_api import pretty_print_response as pprint
- random.seed(2)
+ random.seed(0)
state = State.new_game(2)
ticks = 0
try:
From ec4da64f79111496399763c75efb0035326687bc Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Thu, 28 Nov 2024 20:32:38 -0600
Subject: [PATCH 07/67] Add more tests for sprite
---
src/azul/sprite.py | 10 +++---
tests/test_sprite.py | 72 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 77 insertions(+), 5 deletions(-)
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index 4cff67c..e55ea4e 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -160,7 +160,7 @@ class ImageComponent(ComponentManager):
"""Allow sprite to use multiple images easily.
Components Supplied:
- AnimationComponent
+ # AnimationComponent
OutlineComponent
Requires Component:
@@ -186,7 +186,7 @@ def __init__(self) -> None:
self.add_components(
(
- AnimationComponent(),
+ # AnimationComponent(),
OutlineComponent(),
),
)
@@ -253,7 +253,7 @@ def get_mask(self, identifier: int | str) -> Mask:
while True:
if not self.image_exists(identifier):
raise ValueError(
- f'No image saved for identifier "{identifier}"',
+ f'No mask saved for identifier "{identifier}"',
)
mask = self.__masks[identifier]
if isinstance(mask, Mask):
@@ -785,12 +785,12 @@ def clear_groups(self) -> None:
for group_id in tuple(self.groups):
self.remove_group(group_id)
- def __del__(self) -> None:
+ def __del__(self) -> None: # pragma: nocover
"""Clear groups."""
self.clear_groups()
-def convert_pygame_event(event: PygameEvent) -> Event[Any]:
+def convert_pygame_event(event: PygameEvent) -> Event[Any]: # pragma: nocover
"""Convert Pygame Event to Component Event."""
# data = event.dict
# data['type_int'] = event.type
diff --git a/tests/test_sprite.py b/tests/test_sprite.py
index f2bda6c..9c7ce9a 100644
--- a/tests/test_sprite.py
+++ b/tests/test_sprite.py
@@ -1,8 +1,11 @@
from __future__ import annotations
+from typing import cast
+
import pytest
import trio
from libcomponent.component import Event
+from pygame.mask import Mask
from pygame.rect import Rect
from pygame.surface import Surface
@@ -33,6 +36,7 @@ def image_component(sprite: Sprite) -> ImageComponent:
@pytest.fixture
def animation_component(image_component: ImageComponent) -> AnimationComponent:
+ image_component.add_component(AnimationComponent())
return image_component.get_component("animation")
@@ -113,6 +117,18 @@ def test_sprite_image_set_location_change(sprite: Sprite) -> None:
assert sprite.location == (100, 100)
+def test_sprite_selected_invisible(sprite: Sprite) -> None:
+ assert not sprite.visible
+ sprite.rect.size = (100, 100)
+ assert not sprite.is_selected((20, 20))
+
+
+def test_sprite_selected(sprite: Sprite) -> None:
+ sprite.visible = True
+ sprite.rect.size = (100, 100)
+ assert sprite.is_selected((20, 20))
+
+
def test_image_component_init(image_component: ImageComponent) -> None:
assert image_component.mask_threshold == 127
@@ -123,6 +139,40 @@ def test_image_component_add_image(image_component: ImageComponent) -> None:
assert "test_image" in image_component.list_images()
+def test_image_component_add_images(image_component: ImageComponent) -> None:
+ image = Surface((10, 10))
+ image_component.add_images({"test_image": image})
+ assert "test_image" in image_component.list_images()
+
+
+def test_image_component_get_image_fail(
+ image_component: ImageComponent,
+) -> None:
+ with pytest.raises(
+ ValueError,
+ match='^No image saved for identifier "test_image"$',
+ ):
+ image_component.get_image("test_image")
+
+
+def test_image_component_get_mask_fail(
+ image_component: ImageComponent,
+) -> None:
+ with pytest.raises(
+ ValueError,
+ match='^No mask saved for identifier "test_mask"$',
+ ):
+ image_component.get_mask("test_mask")
+
+
+def test_image_component_get_mask_success(
+ image_component: ImageComponent,
+) -> None:
+ image = Surface((10, 10))
+ image_component.add_image("test_image", image)
+ assert isinstance(image_component.get_mask("test_image"), Mask)
+
+
def test_image_component_add_image_and_mask_invalid_image(
image_component: ImageComponent,
) -> None:
@@ -169,6 +219,28 @@ def test_image_component_add_image_duplication(
assert image_component.get_image("duplicate") is image
+def test_image_component_get_duplicate_mask(
+ image_component: ImageComponent,
+) -> None:
+ image = Surface((1, 1))
+ image_component.add_image("test_image", image)
+ image_component.add_image("duplicate", "test_image")
+ assert isinstance(image_component.get_mask("duplicate"), Mask)
+
+
+def test_image_component_set_image_affects_sprite(
+ image_component: ImageComponent,
+) -> None:
+ image = Surface((1, 1))
+ sprite = cast(Sprite, image_component.manager.get_component("sprite"))
+ image_component.add_image("test_image", image)
+ assert sprite.image is None
+ image_component.set_image("test_image")
+ assert sprite.image is image
+ image_component.set_image("test_image")
+ assert sprite.image is image
+
+
def test_movement_component_init(
movement_component: MovementComponent,
) -> None:
From aae563474e456cf06ece3589bad9f02fd0ac9b90 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 29 Nov 2024 01:08:33 -0600
Subject: [PATCH 08/67] Work on making GUI side work Using beta release of
`libcomponent` for the moment, uses new `unregister_handler` feature
---
pyproject.toml | 2 +-
src/azul/client.py | 25 ++--
src/azul/game.py | 281 +++++++++++++++++++++----------------------
src/azul/server.py | 31 +++--
test-requirements.in | 2 +-
5 files changed, 178 insertions(+), 163 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 53b856d..4012fde 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,7 +36,7 @@ keywords = [
"ai", "multi-player", "azul", "ai-support", "networked-game"
]
dependencies = [
- "libcomponent~=0.0.0",
+ "libcomponent @ git+https://github.com/CoolCat467/LibComponent",
"pygame~=2.6.0",
"typing_extensions>=4.12.2",
"mypy_extensions>=1.0.0",
diff --git a/src/azul/client.py b/src/azul/client.py
index d830f74..35fdbf1 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -162,8 +162,6 @@ def __init__(self, name: str) -> None:
sbe = ServerBoundEvents
self.register_network_write_events(
{
- "select_piece->server": sbe.select_piece,
- "select_tile->server": sbe.select_tile,
"encryption_response->server": sbe.encryption_response,
},
)
@@ -171,16 +169,7 @@ def __init__(self, name: str) -> None:
self.register_read_network_events(
{
cbe.callback_ping: "server->callback_ping",
- cbe.create_piece: "server->create_piece",
- cbe.select_piece: "server->select_piece",
- cbe.create_tile: "server->create_tile",
- cbe.delete_tile: "server->delete_tile",
- cbe.delete_piece_animation: "server->delete_piece_animation",
- cbe.update_piece_animation: "server->update_piece_animation",
- cbe.move_piece_animation: "server->move_piece_animation",
- cbe.animation_state: "server->animation_state",
cbe.game_over: "server->game_over",
- cbe.action_complete: "server->action_complete",
cbe.initial_config: "server->initial_config",
cbe.playing_as: "server->playing_as",
cbe.encryption_request: "server->encryption_request",
@@ -332,11 +321,21 @@ async def read_initial_config(self, event: Event[bytearray]) -> None:
"""Read initial_config event from server."""
buffer = Buffer(event.data)
- board_size: u8 = buffer.read_value(StructFormat.UBYTE)
+ varient_play: u8 = buffer.read_value(StructFormat.BOOL)
+ player_count: u8 = buffer.read_value(StructFormat.UBYTE)
+ factory_count: u8 = buffer.read_value(StructFormat.UBYTE)
current_turn: u8 = buffer.read_value(StructFormat.UBYTE)
await self.raise_event(
- Event("game_initial_config", (board_size, current_turn)),
+ Event(
+ "game_initial_config",
+ (
+ varient_play,
+ player_count,
+ factory_count,
+ current_turn,
+ ),
+ ),
)
async def read_playing_as(self, event: Event[bytearray]) -> None:
diff --git a/src/azul/game.py b/src/azul/game.py
index db0bc45..e9c1b56 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -634,14 +634,26 @@ def __init__(
self.background = background
- def clear_image(self, tile_dimensions: tuple[int, int]) -> None:
+ def clear_image(
+ self,
+ tile_dimensions: tuple[int, int],
+ extra: tuple[int, int] | None = None,
+ ) -> None:
"""Reset self.image using tile_dimensions tuple and fills with self.background. Also updates self.width_height."""
tile_width, tile_height = tile_dimensions
tile_full = self.tile_size + self.tile_separation
+
+ ox = self.tile_separation
+ oy = self.tile_separation
+
+ if extra is not None:
+ ox += extra[0]
+ oy += extra[1]
+
self.image = get_tile_container_image(
(
- round(tile_width * tile_full + self.tile_separation),
- round(tile_height * tile_full + self.tile_separation),
+ round(tile_width * tile_full + ox),
+ round(tile_height * tile_full + oy),
),
self.background,
)
@@ -731,7 +743,10 @@ class Cursor(TileRenderer):
- cursor_drag
- cursor_reached_destination
- cursor_set_destination
- - cursor_set_location
+ - cursor_set_movement_mode
+
+ Sometimes registered:
+ - PygameMouseMotion
"""
__slots__ = ("tiles",)
@@ -771,7 +786,7 @@ def bind_handlers(self) -> None:
"cursor_drag": self.handle_cursor_drag,
"cursor_reached_destination": self.handle_cursor_reached_destination,
"cursor_set_destination": self.handle_cursor_set_destination,
- "cursor_set_location": self.handle_cursor_set_location,
+ "cursor_set_movement_mode": self.handle_cursor_set_movement_mode,
},
)
@@ -813,13 +828,28 @@ async def handle_cursor_set_destination(
self.move_to_front()
await trio.lowlevel.checkpoint()
- async def handle_cursor_set_location(
+ async def handle_pygame_mouse_motion(
self,
- event: Event[tuple[int, int]],
+ event: Event[sprite.PygameMouseMotion],
) -> None:
"""Set location to event data."""
self.move_to_front()
- self.location = event.data
+ self.location = event.data["pos"]
+ await trio.lowlevel.checkpoint()
+
+ async def handle_cursor_set_movement_mode(
+ self,
+ event: Event[bool],
+ ) -> None:
+ """Change cursor movement mode. True if client mode, False if server mode."""
+ client_mode = event.data
+ if client_mode:
+ self.register_handler(
+ "PygameMouseMotion",
+ self.handle_pygame_mouse_motion,
+ )
+ else:
+ self.unregister_handler_type("PygameMouseMotion")
await trio.lowlevel.checkpoint()
def get_held_count(self) -> int:
@@ -881,16 +911,20 @@ def get_tile(self, xy: tuple[int, int]) -> int:
x, y = xy
return int(self.data[y, x])
- def update_image(self) -> None:
+ def update_image(
+ self,
+ offset: tuple[int, int] | None = None,
+ extra_space: tuple[int, int] | None = None,
+ ) -> None:
"""Update self.image."""
- self.clear_image(self.size)
+ self.clear_image(self.size, extra_space)
width, height = self.size
for y in range(height):
for x in range(width):
pos = (x, y)
- self.blit_tile(self.get_tile(pos), pos)
+ self.blit_tile(self.get_tile(pos), pos, offset)
def fake_tile_exists(self, xy: tuple[int, int]) -> bool:
"""Return if tile at given position is a fake tile."""
@@ -928,9 +962,9 @@ class Board(Grid):
__slots__ = ("additions", "variant_play", "wall_tiling")
- def __init__(self, variant_play: bool = False) -> None:
+ def __init__(self, name: str, variant_play: bool = False) -> None:
"""Initialize player's board."""
- super().__init__("Board", (5, 5), background=ORANGE)
+ super().__init__(name, (5, 5), background=ORANGE)
self.variant_play = variant_play
self.additions: dict[int, int | None] = {}
@@ -1504,98 +1538,55 @@ def can_place_tiles(self, tiles: list[int]) -> bool:
class Factory(Grid):
"""Represents a Factory."""
- size = (2, 2)
color = WHITE
outline = BLUE
- out_size = 0.1
def __init__(self, factory_id: int) -> None:
"""Initialize factory."""
- super().__init__(self.size, background=None)
+ super().__init__(f"Factory_{factory_id}", (2, 2), background=None)
+
self.number = factory_id
- self.name = f"Factory{self.number}"
- self.radius = math.ceil(
- self.tile_full * self.size[0] * self.size[1] / 3 + 3,
- )
+ self.redraw()
+ self.visible = True
def __repr__(self) -> str:
"""Return representation of self."""
return f"{self.__class__.__name__}({self.number})"
- def add_circle(self, surface: pygame.surface.Surface) -> None:
- """Add circle to self.image."""
- rad = math.ceil(self.radius)
- surf = pygame.surface.Surface((2 * rad, 2 * rad), SRCALPHA)
- pygame.draw.circle(surf, self.outline, (rad, rad), rad)
+ def clear_image(
+ self,
+ tile_size: tuple[int, int],
+ extra_space: tuple[int, int] | None,
+ ) -> None:
+ """Clear self.image and draw circles."""
+ super().clear_image(tile_size, extra_space)
+ radius = 29
pygame.draw.circle(
- surf,
- self.color,
- (rad, rad),
- math.ceil(rad * (1 - self.out_size)),
+ self.image,
+ self.outline,
+ (radius, radius),
+ radius,
)
-
- surface.blit(
- surf,
- (
- round(self.location[0] - self.radius),
- round(self.location[1] - self.radius),
- ),
+ pygame.draw.circle(
+ self.image,
+ self.color,
+ (radius, radius),
+ math.ceil(radius * 0.9),
)
- def render(self, surface: pygame.surface.Surface) -> None:
- """Render Factory."""
- if not self.hidden:
- self.add_circle(surface)
- super().render(surface)
-
- def fill(self, tiles: list[int]) -> None:
- """Fill self with tiles. Will raise exception if insufficiant tiles."""
- if len(tiles) < self.size[0] * self.size[1]:
- size = self.size[0] * self.size[1]
- raise RuntimeError(
- f"Insufficiant quantity of tiles! Needs {size}!",
- )
- for y in range(self.size[1]):
- for tile, x in zip(
- (tiles.pop() for i in range(self.size[0])),
- range(self.size[0]),
- strict=True,
- ):
- self.place_tile((x, y), tile)
- if tiles:
- raise RuntimeError("Too many tiles!")
-
- def grab(self) -> list[int]:
- """Return all tiles on this factory."""
- return [
- tile
- for tile in (
- self.get_tile((x, y), Tile.blank)
- for x in range(self.size[0])
- for y in range(self.size[1])
- )
- if tile is not None and tile.color != Tile.blank
- ]
-
- def grab_color(self, color: int) -> tuple[list[int], list[int]]:
- """Return all tiles of color given in the first list, and all non-matches in the second list."""
- tiles = self.grab()
- right, wrong = [], []
- for tile in tiles:
- if tile.color == color:
- right.append(tile)
- else:
- wrong.append(tile)
- return right, wrong
+ def redraw(self) -> None:
+ """Redraw this factory."""
+ super().update_image(offset=(8, 8), extra_space=(16, 16))
- def process(self, time_passed: float) -> None:
- """Process self."""
- if self.image_update:
- self.radius = int(
- self.tile_full * self.size[0] * self.size[1] // 3 + 3,
- )
- super().process(time_passed)
+ def get_tile_point(
+ self,
+ screen_location: tuple[int, int] | Vector2,
+ ) -> tuple[int, int] | None:
+ """Get tile point accounting for offset."""
+ return super().get_tile_point(
+ Vector2.from_iter(screen_location) - (8, 8),
+ )
class Factories(MultipartObject):
@@ -1643,7 +1634,7 @@ def reset_position(self) -> None:
self.objects[index].location = (
Vector2.from_degrees(
degrees,
- self.size,
+ 29 * 5,
)
+ self.location
)
@@ -2356,55 +2347,6 @@ async def check_conditions(self) -> str:
return "title"
-class InitializeState(GameState):
- """Initialize state."""
-
- __slots__ = ()
-
- def __init__(self) -> None:
- """Initialize self."""
- super().__init__("initialize")
-
- async def entry_actions(self) -> None:
- """Set up buttons."""
- assert self.machine is not None
- self.id = self.machine.new_group("initialize")
-
- self.group_add(Cursor())
- await self.manager.raise_event(Event("cursor_drag", [3, 5]))
- self.manager.register_handler("PygameMouseMotion", self.mouse_moved)
-
- ## board = Board()
- #### board.place_tile((2, 2), Tile.red)
- ## board.location = Vector2.from_iter(SCREEN_SIZE) // 2
- ## self.group_add(board)
-
- center = TableCenter()
- center.location = Vector2.from_iter(SCREEN_SIZE) // 2
- self.group_add(center)
- center.add_tiles((0, 1, 2, 3, 5))
-
- async def mouse_moved(
- self,
- event: Event[sprite.PygameMouseMotion],
- ) -> None:
- """Handle PygameMouseMotion event."""
- ## print(f'{event = }')
- await self.manager.raise_event(
- Event("cursor_set_location", event.data["pos"]),
- )
-
-
-## await self.manager.raise_event(
-## Event("cursor_set_destination", event.data["pos"]),
-## )
-
-
-## async def check_conditions(self) -> str:
-## """Go to title state."""
-## return "title"
-
-
class TitleState(MenuState):
"""Game state when the title screen is up."""
@@ -3267,6 +3209,23 @@ async def handle_return_to_title(self, _: Event[None]) -> None:
# return None
+## async def entry_actions(self) -> None:
+## """Set up buttons."""
+## assert self.machine is not None
+## self.id = self.machine.new_group("initialize")
+##
+## self.group_add(Cursor())
+## await self.manager.raise_event(Event("cursor_drag", [3, 5]))
+## self.manager.register_handler("PygameMouseMotion", self.mouse_moved)
+##
+## ## board = Board()
+## #### board.place_tile((2, 2), Tile.red)
+## ## board.location = Vector2.from_iter(SCREEN_SIZE) // 2
+## ## self.group_add(board)
+##
+## center.add_tiles((0, 1, 2, 3, 5))
+
+
class PlayState(GameState):
"""Game Play State."""
@@ -3283,6 +3242,7 @@ def register_handlers(self) -> None:
"""Register event handlers."""
self.manager.register_handlers(
{
+ "game_initial_config": self.handle_game_initial_config,
"client_disconnected": self.handle_client_disconnected,
"game_winner": self.handle_game_over,
},
@@ -3295,12 +3255,16 @@ def add_actions(self) -> None:
async def entry_actions(self) -> None:
"""Add GameBoard and raise init event."""
- self.exit_data = None
-
assert self.machine is not None
if self.id == 0:
self.id = self.machine.new_group("play")
+ self.group_add(Cursor())
+
+ center = TableCenter()
+ center.location = Vector2.from_iter(SCREEN_SIZE) // 2
+ self.group_add(center)
+
# self.group_add(())
## gameboard = GameBoard(
## 45,
@@ -3308,7 +3272,42 @@ async def entry_actions(self) -> None:
## gameboard.location = [x // 2 for x in SCREEN_SIZE]
## self.group_add(gameboard)
- await self.machine.raise_event(Event("init", None))
+ async def handle_game_initial_config(
+ self,
+ event: Event[tuple[bool, int, int, int]],
+ ) -> None:
+ """Handle `game_initial_config` event."""
+ varient_play, player_count, factory_count, current_turn = event.data
+
+ print("handle_game_initial_config")
+ print((varient_play, player_count, factory_count, current_turn))
+
+ center = Vector2.from_iter(SCREEN_SIZE) // 2
+
+ # Add factories
+ for index, degrees in enumerate(range(0, 360, 360 // factory_count)):
+ factory = Factory(index)
+ factory.location = (
+ Vector2.from_degrees(
+ degrees - 90,
+ 145,
+ )
+ + center
+ )
+ self.group_add(factory)
+
+ # Add players
+ # TODO: Do it properly
+ for index, degrees in enumerate(range(0, 360, 360 // player_count)):
+ board = Board(f"Board_{index}", varient_play)
+ board.location = (
+ Vector2.from_degrees(
+ degrees - 45,
+ 300,
+ )
+ + center
+ )
+ self.group_add(board)
async def check_conditions(self) -> str | None:
"""Return to title if client component doesn't exist."""
diff --git a/src/azul/server.py b/src/azul/server.py
index 4cd2226..13a9397 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -60,6 +60,10 @@
from collections.abc import Awaitable, Callable
+# cursor_set_movement_mode
+# cursor_set_destination
+
+
class ServerClient(ServerClientNetworkEventComponent):
"""Server Client Network Event Component.
@@ -119,16 +123,17 @@ async def handle_game_over(self, event: Event[int]) -> None:
async def handle_initial_config(
self,
- event: Event[tuple[None, int]],
+ event: Event[tuple[bool, int, int, int]],
) -> None:
"""Read initial config event and reraise as server[write]->initial_config."""
- board_size, player_turn = event.data
+ varient_play, player_count, factory_count, current_turn = event.data
buffer = Buffer()
- ## write_position(buffer, board_size)
- buffer.write_value(StructFormat.UBYTE, 0)
- buffer.write_value(StructFormat.UBYTE, player_turn)
+ buffer.write_value(StructFormat.BOOL, varient_play)
+ buffer.write_value(StructFormat.UBYTE, player_count)
+ buffer.write_value(StructFormat.UBYTE, factory_count)
+ buffer.write_value(StructFormat.UBYTE, current_turn)
await self.write_event(Event("server[write]->initial_config", buffer))
@@ -330,6 +335,7 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]:
def new_game_init(self, varient_play: bool = False) -> None:
"""Start new game."""
+ print("server new_game_init")
self.client_players.clear()
self.state = State.new_game(
@@ -410,7 +416,12 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
await self.raise_event(
Event(
"initial_config->network",
- (None, self.state.turn),
+ (
+ self.state.varient_play,
+ len(self.state.player_data),
+ len(self.state.factory_displays),
+ self.state.current_turn,
+ ),
),
)
@@ -493,7 +504,12 @@ async def send_spectator_join_packets(
await client.raise_event(
Event(
"initial_config->network",
- (self.state.varient_play, self.state.current_turn),
+ (
+ self.state.varient_play,
+ len(self.state.player_data),
+ len(self.state.factory_displays),
+ self.state.current_turn,
+ ),
),
)
@@ -506,6 +522,7 @@ async def handler(self, stream: trio.SocketStream) -> None:
if self.client_count == 0 and self.game_active():
# Old game was running but everyone left, restart
print("TODO: restart")
+ self.new_game_init()
new_client_id = self.client_count
# Is controlling player?
diff --git a/test-requirements.in b/test-requirements.in
index 0094b39..a9dc141 100644
--- a/test-requirements.in
+++ b/test-requirements.in
@@ -19,7 +19,7 @@ typing-extensions
# Azul's own dependencies
#
exceptiongroup; python_version < '3.11'
-libcomponent~=0.0.0
+libcomponent @ git+https://github.com/CoolCat467/LibComponent
mypy_extensions>=1.0.0
numpy~=2.1.3
pygame~=2.6.0
From 2ee1522160679df5617f1e0fcf72e37c3be1b598 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 29 Nov 2024 13:52:54 -0600
Subject: [PATCH 09/67] Send board data and get rid of old code
---
src/azul/client.py | 41 +-
src/azul/game.py | 1504 ++----------------------------------
src/azul/network_shared.py | 28 +-
src/azul/server.py | 108 +--
src/azul/state.py | 14 +
5 files changed, 185 insertions(+), 1510 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index 35fdbf1..b9a1d1a 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -40,6 +40,7 @@
ADVERTISEMENT_PORT,
ClientBoundEvents,
ServerBoundEvents,
+ decode_int8_array,
)
if TYPE_CHECKING:
@@ -168,11 +169,12 @@ def __init__(self, name: str) -> None:
cbe = ClientBoundEvents
self.register_read_network_events(
{
+ cbe.encryption_request: "server->encryption_request",
cbe.callback_ping: "server->callback_ping",
- cbe.game_over: "server->game_over",
cbe.initial_config: "server->initial_config",
cbe.playing_as: "server->playing_as",
- cbe.encryption_request: "server->encryption_request",
+ cbe.game_over: "server->game_over",
+ cbe.board_data: "server->board_data",
},
)
@@ -184,14 +186,14 @@ def bind_handlers(self) -> None:
super().bind_handlers()
self.register_handlers(
{
+ "server->encryption_request": self.read_encryption_request,
"server->callback_ping": self.read_callback_ping,
- "server->game_over": self.read_game_over,
"server->initial_config": self.read_initial_config,
"server->playing_as": self.read_playing_as,
- "server->encryption_request": self.read_encryption_request,
- "network_stop": self.handle_network_stop,
+ "server->game_over": self.read_game_over,
+ "server->board_data": self.read_board_data,
"client_connect": self.handle_client_connect,
- # f"client[{self.name}]_read_event": self.handle_read_event,
+ "network_stop": self.handle_network_stop,
},
)
@@ -308,15 +310,6 @@ async def handle_client_connect(
return
await self.raise_disconnect("Error connecting to server.")
- async def read_game_over(self, event: Event[bytearray]) -> None:
- """Read update_piece event from server."""
- buffer = Buffer(event.data)
-
- winner: u8 = buffer.read_value(StructFormat.UBYTE)
-
- await self.raise_event(Event("game_winner", winner))
- self.running = False
-
async def read_initial_config(self, event: Event[bytearray]) -> None:
"""Read initial_config event from server."""
buffer = Buffer(event.data)
@@ -348,6 +341,24 @@ async def read_playing_as(self, event: Event[bytearray]) -> None:
Event("game_playing_as", playing_as),
)
+ async def read_game_over(self, event: Event[bytearray]) -> None:
+ """Read game_over event from server."""
+ buffer = Buffer(event.data)
+
+ winner: u8 = buffer.read_value(StructFormat.UBYTE)
+
+ await self.raise_event(Event("game_winner", winner))
+ self.running = False
+
+ async def read_board_data(self, event: Event[bytearray]) -> None:
+ """Read board_data event from server."""
+ buffer = Buffer(event.data)
+
+ player_id: u8 = buffer.read_value(StructFormat.UBYTE)
+ array = decode_int8_array(buffer, (5, 5))
+
+ await self.raise_event(Event("game_board_data", (player_id, array)))
+
async def handle_network_stop(self, event: Event[None]) -> None:
"""Send EOF if connected and close socket."""
if self.not_connected:
diff --git a/src/azul/game.py b/src/azul/game.py
index e9c1b56..59c288f 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -21,14 +21,13 @@
__title__ = "Azul"
__author__ = "CoolCat467"
+__license__ = "GNU General Public License Version 3"
__version__ = "2.0.0"
import contextlib
import importlib
import math
-import operator
import os
-import random
import sys
import time
import traceback
@@ -68,7 +67,6 @@
from azul.statemachine import AsyncState
from azul.tools import (
lerp_color,
- saturate,
)
from azul.vector import Vector2
@@ -84,6 +82,7 @@
Sequence,
)
+ from numpy.typing import NDArray
from typing_extensions import TypeVarTuple
P = TypeVarTuple("P")
@@ -392,225 +391,6 @@ def get_tile_container_image(
return image
-class ObjectHandler:
- """ObjectHandler class, meant to be used for other classes."""
-
- # __slots__ = ("objects", "next_id", "cache")
-
- def __init__(self) -> None:
- """Initialize object handler."""
- self.objects: dict[int, sprite.Sprite] = {}
- self.next_id = 0
- self.cache: dict[str, int] = {}
-
- self.recalculate_render = True
- self._render_order: tuple[int, ...] = ()
-
- def add_object(self, obj: sprite.Sprite) -> None:
- """Add an object to the game."""
- obj.id = self.next_id
- self.objects[self.next_id] = obj
- self.next_id += 1
- self.recalculate_render = True
-
- def rm_object(self, obj: sprite.Sprite) -> None:
- """Remove an object from the game."""
- del self.objects[obj.id]
- self.recalculate_render = True
-
- def rm_star(self) -> None:
- """Remove all objects from self.objects."""
- for oid in list(self.objects):
- self.rm_object(self.objects[oid])
- self.next_id = 0
-
- def get_object(self, object_id: int) -> sprite.Sprite | None:
- """Return the object associated with object id given. Return None if object not found."""
- if object_id in self.objects:
- return self.objects[object_id]
- return None
-
- def get_objects_with_attr(self, attribute: str) -> tuple[int, ...]:
- """Return a tuple of object ids with given attribute."""
- return tuple(
- oid
- for oid in self.objects
- if hasattr(self.objects[oid], attribute)
- )
-
- def get_object_by_attr(
- self,
- attribute: str,
- value: object,
- ) -> tuple[int, ...]:
- """Return a tuple of object ids with that are equal to ."""
- matches = []
- for oid in self.get_objects_with_attr(attribute):
- if getattr(self.objects[oid], attribute) == value:
- matches.append(oid)
- return tuple(matches)
-
- def get_object_given_name(self, name: str) -> tuple[int, ...]:
- """Return a tuple of object ids with names matching ."""
- return self.get_object_by_attr("name", name)
-
- def reset_cache(self) -> None:
- """Reset the cache."""
- self.cache = {}
-
- def get_object_by_name(self, object_name: str) -> sprite.Sprite:
- """Get object by name, with cache."""
- if object_name not in self.cache:
- ids = self.get_object_given_name(object_name)
- if ids:
- self.cache[object_name] = min(ids)
- else:
- raise RuntimeError(f"{object_name} sprite.Sprite Not Found!")
- result = self.get_object(self.cache[object_name])
- if result is None:
- raise RuntimeError(f"{object_name} sprite.Sprite Not Found!")
- return result
-
- def set_attr_all(self, attribute: str, value: object) -> None:
- """Set given attribute in all of self.objects to given value in all objects with that attribute."""
- for oid in self.get_objects_with_attr(attribute):
- setattr(self.objects[oid], attribute, value)
-
- def recalculate_render_order(self) -> None:
- """Recalculate the order in which to render objects to the screen."""
- new: dict[int, int] = {}
- cur = 0
- for oid in reversed(self.objects):
- obj = self.objects[oid]
- if hasattr(obj, "Render_Priority"):
- prior = obj.Render_Priority
- if isinstance(prior, str):
- add = 0
- if prior[:4] == "last":
- try:
- add = int(prior[4:] or 0)
- except ValueError:
- add = 0
- pos = len(self.objects) + add
- if prior[:5] == "first":
- try:
- add = int(prior[5:] or 0)
- except ValueError:
- add = 0
- pos = -1 + add
- if pos not in new.values():
- new[oid] = pos
- else:
- while True:
- if add < 0:
- pos -= 1
- else:
- pos += 1
- if pos not in new.values():
- new[oid] = pos
- break
- else:
- try:
- prior = int(prior)
- except ValueError:
- prior = cur
- while True:
- if prior in new.values():
- prior += 1
- else:
- break
- new[oid] = prior
- else:
- while True:
- if cur in new.values():
- cur += 1
- else:
- break
- new[oid] = cur
- cur += 1
- revnew = {new[k]: k for k in new}
- self._render_order = tuple(revnew[key] for key in sorted(revnew))
-
- def process_objects(self, time_passed: float) -> None:
- """Call the process function on all objects."""
- if self.recalculate_render:
- self.recalculate_render_order()
- self.recalculate_render = False
- for oid in iter(self.objects):
- self.objects[oid].process(time_passed)
-
- def render_objects(self, surface: pygame.surface.Surface) -> None:
- """Render all objects to surface."""
- if not self._render_order or self.recalculate_render:
- self.recalculate_render_order()
- self.recalculate_render = False
- for oid in self._render_order: # reversed(list(self.objects.keys())):
- self.objects[oid].render(surface)
-
- def __del__(self) -> None:
- """Cleanup."""
- self.reset_cache()
- self.rm_star()
-
-
-class MultipartObject(ObjectHandler):
- """Thing that is both an sprite.Sprite and an ObjectHandler, and is meant to be an sprite.Sprite made up of multiple Objects."""
-
- def __init__(self, name: str):
- """Initialize sprite.Sprite and ObjectHandler of self.
-
- Also set self._lastloc and self._lasthidden to None
- """
- ObjectHandler.__init__(self)
-
- self._lastloc: Vector2 | None = None
- self._lasthidden: bool | None = None
-
- def reset_position(self) -> None:
- """Reset the position of all objects within."""
- raise NotImplementedError
-
- def get_intersection(
- self,
- point: tuple[int, int] | Vector2,
- ) -> tuple[str, tuple[int, int]] | tuple[None, None]:
- """Return where a given point touches in self. Returns (None, None) with no intersections."""
- for oid in self.objects:
- obj = self.objects[oid]
- if hasattr(obj, "get_tile_point"):
- output = obj.get_tile_point(point)
- if output is not None:
- return obj.name, output
- else:
- raise Warning(
- "Not all of self.objects have the get_tile_point attribute!",
- )
- return None, None
-
- def process(self, time_passed: float) -> None:
- """Process sprite.Sprite self and ObjectHandler self and call self.reset_position on location change."""
- sprite.Sprite.process(self, time_passed)
- ObjectHandler.process_objects(self, time_passed)
-
- if self.location != self._lastloc:
- self.reset_position()
- self._lastloc = self.location
-
- if self.hidden != self._lasthidden:
- self.set_attr_all("hidden", self.hidden)
- self._lasthidden = self.hidden
-
- def render(self, surface: pygame.surface.Surface) -> None:
- """Render self and all parts to the surface."""
- sprite.Sprite.render(self, surface)
- ObjectHandler.render_objects(self, surface)
-
- def __del__(self) -> None:
- """Delete data."""
- sprite.Sprite.__del__(self)
- ObjectHandler.__del__(self)
-
-
class TileRenderer(sprite.Sprite):
"""Base class for all objects that need to render tiles."""
@@ -640,21 +420,19 @@ def clear_image(
extra: tuple[int, int] | None = None,
) -> None:
"""Reset self.image using tile_dimensions tuple and fills with self.background. Also updates self.width_height."""
- tile_width, tile_height = tile_dimensions
+ size = Vector2.from_iter(tile_dimensions)
tile_full = self.tile_size + self.tile_separation
+ size *= tile_full
- ox = self.tile_separation
- oy = self.tile_separation
+ offset = Vector2(self.tile_separation, self.tile_separation)
if extra is not None:
- ox += extra[0]
- oy += extra[1]
+ offset += extra
+
+ size += offset
self.image = get_tile_container_image(
- (
- round(tile_width * tile_full + ox),
- round(tile_height * tile_full + oy),
- ),
+ round(size),
self.background,
)
@@ -721,21 +499,6 @@ def get_tile_point(
return tile_position
-## def screen_size_update(self) -> None:
-## """Handle screensize is changes."""
-## nx, ny = self.location
-##
-## if self.location_mode_on_resize == "Scale":
-## ow, oh = self.screen_size_last
-## nw, nh = SCREEN_SIZE
-##
-## x, y = self.location
-## nx, ny = x * (nw / ow), y * (nh / oh)
-##
-## self.location = Vector2(nx, ny)
-## self.screen_size_last = SCREEN_SIZE
-
-
class Cursor(TileRenderer):
"""Cursor TileRenderer.
@@ -960,289 +723,69 @@ def is_empty(self, empty_color: int = Tile.blank) -> bool:
class Board(Grid):
"""Represents the board in the Game."""
- __slots__ = ("additions", "variant_play", "wall_tiling")
+ __slots__ = ("board_id",)
- def __init__(self, name: str, variant_play: bool = False) -> None:
+ def __init__(self, board_id: int) -> None:
"""Initialize player's board."""
- super().__init__(name, (5, 5), background=ORANGE)
+ super().__init__(f"board_{board_id}", (5, 5), background=ORANGE)
- self.variant_play = variant_play
- self.additions: dict[int, int | None] = {}
+ self.board_id = board_id
- self.wall_tiling = False
-
- if not variant_play:
- self.set_colors()
- else:
- self.update_image()
- self.visible = True
+ self.update_location_on_resize = True
def __repr__(self) -> str:
"""Return representation of self."""
return f"{self.__class__.__name__}({self.variant_play})"
- def set_colors(self, keep_real: bool = True) -> None:
- """Reset tile colors."""
- width, height = self.size
- for y in range(height):
- for x in range(width):
- if not keep_real or self.fake_tile_exists((x, y)):
- color = -((height - y + x) % REGTILECOUNT + 1)
- self.data[y, x] = color
- self.update_image()
-
- def get_row(self, index: int) -> Generator[int, None, None]:
- """Return a row from self. Does not delete data from internal grid."""
- for x in range(self.size[0]):
- yield self.get_info((x, index))
-
- def get_column(self, index: int) -> Generator[int, None, None]:
- """Return a column from self. Does not delete data from internal grid."""
- for y in range(self.size[1]):
- yield self.get_info((index, y))
-
- def get_colors_in_row(
- self,
- index: int,
- exclude_negatives: bool = True,
- ) -> set[int]:
- """Return the colors placed in a given row in internal grid."""
- row_colors: Iterable[int] = self.get_row(index)
- if exclude_negatives:
- row_colors = (c for c in row_colors if c >= 0)
- return set(row_colors)
-
- def get_colors_in_column(
- self,
- index: int,
- exclude_negatives: bool = True,
- ) -> set[int]:
- """Return the colors placed in a given row in internal grid."""
- column_colors: Iterable[int] = self.get_column(index)
- if exclude_negatives:
- column_colors = (c for c in column_colors if c >= 0)
- return set(column_colors)
-
- def is_wall_tiling(self) -> bool:
- """Return True if in Wall Tiling Mode."""
- return self.wall_tiling
-
- def can_place_tile_color_at_point(
- self,
- position: tuple[int, int],
- tile_color: int,
- ) -> bool:
- """Return True if tile's color is valid at given position."""
- column, row = position
- colors = self.get_colors_in_column(column) | self.get_colors_in_row(
- row,
+ def bind_handlers(self) -> None:
+ """Register event handlers."""
+ self.register_handlers(
+ {
+ "game_board_data": self.handle_game_board_data,
+ },
)
- return tile_color not in colors
-
- ## def remove_invalid_additions(self) -> None:
- ## """Remove invalid additions that would not be placeable."""
- ## # In the wall-tiling phase, it may happen that you
- ## # are not able to move the rightmost tile of a certain
- ## # pattern line over to the wall because there is no valid
- ## # space left for it. In this case, you must immediately
- ## # place all tiles of that pattern line in your floor line.
- ## for row in range(self.size[1]):
- ## row_tile = self.additions[row]
- ## if not isinstance(row_tile, int):
- ## continue
- ## valid = self.calculate_valid_locations_for_tile_row(row)
- ## if not valid:
- ## floor = self.player.get_object_by_name("floor_line")
- ## assert isinstance(floor, FloorLine)
- ## floor.place_tile(row_tile)
- ## self.additions[row] = None
-
- ## def wall_tile_from_point(self, position: tuple[int, int]) -> bool:
- ## """Given a position, wall tile. Return success on placement. Also updates if in wall tiling mode."""
- ## success = False
- ## column, row = position
- ## at_point = self.get_info(position)
- ## assert at_point is not None
- ## if at_point.color <= 0 and row in self.additions:
- ## tile = self.additions[row]
- ## if isinstance(tile, int) and self.can_place_tile_color_at_point(
- ## position,
- ## tile,
- ## ):
- ## self.place_tile(position, tile)
- ## self.additions[row] = column
- ## # Update invalid placements after new placement
- ## self.remove_invalid_additions()
- ## success = True
- ## if not self.get_rows_to_tile_map():
- ## self.wall_tiling = False
- ## return success
-
- ## def wall_tiling_mode(self, moved_table: dict[int, int]) -> None:
- ## """Set self into Wall Tiling Mode. Finishes automatically if not in variant play mode."""
- ## self.wall_tiling = True
- ## for key, value in moved_table.items():
- ## key = int(key) - 1
- ## if key in self.additions:
- ## raise RuntimeError(
- ## f"Key {key!r} Already in additions dictionary!",
- ## )
- ## self.additions[key] = value
- ## if not self.variant_play:
- ## for row in range(self.size[1]):
- ## if row in self.additions:
- ## rowdata = [tile.color for tile in self.get_row(row)]
- ## tile = self.additions[row]
- ## if not isinstance(tile, int):
- ## continue
- ## negative_tile_color = -(tile.color + 1)
- ## if negative_tile_color in rowdata:
- ## column = rowdata.index(negative_tile_color)
- ## self.place_tile((column, row), tile)
- ## # Set data to the column placed in, use for scoring
- ## self.additions[row] = column
- ## else:
- ## raise RuntimeError(
- ## f"{negative_tile_color} not in row {row}!",
- ## )
- ## else:
- ## raise RuntimeError(f"{row} not in moved_table!")
- ## self.wall_tiling = False
- ## else:
- ## # Invalid additions can only happen in variant play mode.
- ## self.remove_invalid_additions()
-
- def get_touches_continuous(
+
+ async def handle_game_board_data(
self,
- xy: tuple[int, int],
- ) -> tuple[list[int], list[int]]:
- """Return two lists, each of which contain all the tiles that touch the tile at given x y position, including that position."""
- rs, cs = self.size
- x, y = xy
- # Get row and column tile color data
- row = list(self.get_row(y))
- column = list(self.get_column(x))
-
- # Both
- def get_greater_than(v: int, size: int, data: list[int]) -> list[int]:
- """Go through data forward and backward from point v out by size, and return all points from data with a value >= 0."""
-
- def try_range(range_: Iterable[int]) -> list[int]:
- """Try range. Return all of data in range up to when indexed value is < 0."""
- ret = []
- for tv in range_:
- if data[tv] < 0:
- break
- ret.append(tv)
- return ret
-
- nt = try_range(reversed(range(v)))
- pt = try_range(range(v + 1, size))
- return nt + pt
-
- def comb(one: Iterable[T], two: Iterable[RT]) -> list[tuple[T, RT]]:
- """Combine two lists by zipping together and returning list object."""
- return list(zip(one, two, strict=False))
-
- def get_all(lst: list[tuple[int, int]]) -> Generator[int, None, None]:
- """Return all of the self.get_info points for each value in lst."""
- for pos in lst:
- yield self.get_info(pos)
-
- # Get row touches
- row_touches = comb(get_greater_than(x, rs, row), [y] * rs)
- # Get column touches
- column_touches = comb([x] * cs, get_greater_than(y, cs, column))
- # Get real tiles from indexes and return
- return list(get_all(row_touches)), list(get_all(column_touches))
-
- def score_additions(self) -> int:
- """Return the number of points the additions scored.
-
- Uses self.additions, which is set in self.wall_tiling_mode()
- """
- score = 0
- for x, y in ((self.additions[y], y) for y in range(self.size[1])):
- if x is not None:
- assert isinstance(x, int)
- rowt, colt = self.get_touches_continuous((x, y))
- horiz = len(rowt)
- verti = len(colt)
- if horiz > 1:
- score += horiz
- if verti > 1:
- score += verti
- if horiz <= 1 and verti <= 1:
- score += 1
- del self.additions[y]
- return score
-
- def get_filled_rows(self) -> int:
- """Return the number of filled rows on this board."""
- count = 0
- for row in range(self.size[1]):
- real = (t >= 0 for t in self.get_row(row))
- if all(real):
- count += 1
- return count
+ event: Event[tuple[int, NDArray[int8]]],
+ ) -> None:
+ """Handle `game_board_data` event."""
+ board_id, array = event.data
- def has_filled_row(self) -> bool:
- """Return True if there is at least one completely filled horizontal line."""
- return self.get_filled_rows() >= 1
+ if board_id != self.board_id:
+ await trio.lowlevel.checkpoint()
+ return
- def get_filled_columns(self) -> int:
- """Return the number of filled rows on this board."""
- count = 0
- for column in range(self.size[0]):
- real = (t >= 0 for t in self.get_column(column))
- if all(real):
- count += 1
- return count
-
- def get_filled_colors(self) -> int:
- """Return the number of completed colors on this board."""
- color_count = Counter(
- self.get_info((x, y))
- for x in range(self.size[0])
- for y in range(self.size[1])
- )
- count = 0
- for fill_count in color_count.values():
- if fill_count >= 5:
- count += 1
- return count
+ self.data = array
+ self.update_image()
+ self.visible = True
- def end_of_game_scoreing(self) -> int:
- """Return the additional points for this board at the end of the game."""
- score = 0
- score += self.get_filled_rows() * 2
- score += self.get_filled_columns() * 7
- score += self.get_filled_colors() * 10
- return score
+ await trio.lowlevel.checkpoint()
class Row(TileRenderer):
"""Represents one of the five rows each player has."""
- __slots__ = ("color", "player", "size", "tiles")
+ __slots__ = ("color", "size", "tiles")
greyshift = GREYSHIFT
def __init__(
self,
+ name: str,
size: int,
tile_separation: int | None = None,
background: tuple[int, int, int] | None = None,
) -> None:
"""Initialize row."""
super().__init__(
- "Row",
+ name,
tile_separation,
background,
)
- self.size = int(size)
self.color = Tile.blank
- self.tiles = list([self.color] * self.size)
+ self.size = int(size)
+ self.count = 0
def __repr__(self) -> str:
"""Return representation of self."""
@@ -1252,19 +795,24 @@ def update_image(self) -> None:
"""Update self.image."""
self.clear_image((self.size, 1))
- for x in range(len(self.tiles)):
- self.blit_tile(self.tiles[x], (x, 0))
+ for x in range(self.count):
+ self.blit_tile(self.color, (x, 0))
+ for x in range(self.count, self.size):
+ self.blit_tile(Tile.blank, (x, 0))
- def get_tile_point(self, screen_location: tuple[int, int]) -> int | None:
+ def get_tile_point(
+ self,
+ screen_location: tuple[int, int] | Vector2,
+ ) -> int | None:
"""Return the xy choordinates of which tile intersects given a point. Returns None if no intersections."""
- pos = super().get_tile_point()
+ pos = super().get_tile_point(screen_location)
if pos is None:
return None
return pos[0]
def get_placed(self) -> int:
"""Return the number of tiles in self that are not fake tiles, like grey ones."""
- return len([tile for tile in self.tiles if tile.color >= 0])
+ return self.count
def get_placeable(self) -> int:
"""Return the number of tiles permitted to be placed on self."""
@@ -1272,94 +820,7 @@ def get_placeable(self) -> int:
def is_full(self) -> bool:
"""Return True if this row is full."""
- return self.get_placed() == self.size
-
- def get_info(self, location: int) -> int | None:
- """Return tile at location without deleting it. Return None on invalid location."""
- index = self.size - 1 - location
- if index < 0 or index > len(self.tiles):
- return None
- return self.tiles[index]
-
- def can_place(self, tile: int) -> bool:
- """Return True if permitted to place given tile object on self."""
- placeable = (tile.color == self.color) or (
- self.color < 0 and tile.color >= 0
- )
- if not placeable:
- return False
- color_correct = tile.color >= 0 and tile.color < 5
- if not color_correct:
- return False
- number_correct = self.get_placeable() > 0
- if not number_correct:
- return False
-
- board = self.player.get_object_by_name("Board")
- assert isinstance(board, Board)
- # Is color not present?
- return tile.color not in board.get_colors_in_row(
- self.size - 1,
- )
-
- def get_tile(self, replace: int = Tile.blank) -> int:
- """Return the leftmost tile while deleting it from self."""
- self.tiles.appendleft(int(replace))
- self.image_update = True
- return self.tiles.pop()
-
- def place_tile(self, tile: int) -> None:
- """Place a given int sprite.Sprite on self if permitted."""
- if self.can_place(tile):
- self.color = tile.color
- self.tiles.append(tile)
- end = self.tiles.popleft()
- if not end.color < 0:
- raise RuntimeError(
- "Attempted deletion of real tile from Row!",
- )
- self.image_update = True
- else:
- raise ValueError("Not allowed to place.")
-
- def can_place_tiles(self, tiles: list[int]) -> bool:
- """Return True if permitted to place all of given tiles objects on self."""
- if len(tiles) > self.get_placeable():
- return False
- for tile in tiles:
- if not self.can_place(tile):
- return False
- tile_colors = []
- for tile in tiles:
- if tile.color not in tile_colors:
- tile_colors.append(tile.color)
- return not len(tile_colors) > 1
-
- def place_tiles(self, tiles: list[int]) -> None:
- """Place multiple tile objects on self if permitted."""
- if self.can_place_tiles(tiles):
- for tile in tiles:
- self.place_tile(tile)
- else:
- raise ValueError("Not allowed to place tiles.")
-
- ## def wall_tile(
- ## self,
- ## add_to_table: dict[str, list[int] | int | None],
- ## empty_color: int = Tile.blank,
- ## ) -> None:
- ## """Move tiles around and into add dictionary for the wall tiling phase of the game. Removes tiles from self."""
- ## if "tiles_for_box" not in add_to_table:
- ## add_to_table["tiles_for_box"] = []
- ## if not self.is_full():
- ## add_to_table[str(self.size)] = None
- ## return
- ## self.color = empty_color
- ## add_to_table[str(self.size)] = self.get_tile()
- ## for_box = add_to_table["tiles_for_box"]
- ## assert isinstance(for_box, list)
- ## for _i in range(self.size - 1):
- ## for_box.append(self.get_tile())
+ return self.get_placeable() == 0
def set_background(self, color: tuple[int, int, int] | None) -> None:
"""Set the background color for this row."""
@@ -1367,99 +828,14 @@ def set_background(self, color: tuple[int, int, int] | None) -> None:
self.update_image()
-class PatternLine(MultipartObject):
- """Represents multiple rows to make the pattern line."""
-
- __slots__ = ("player", "row_separation")
- size = (5, 5)
-
- def __init__(self, player: Player, row_separation: int = 0) -> None:
- """Initialize pattern line."""
- super().__init__("PatternLine")
- self.player = player
- self.row_separation = row_separation
-
- for x, _y in zip(
- range(self.size[0]),
- range(self.size[1]),
- strict=True,
- ):
- self.add_object(Row(self.player, x + 1))
-
- self.set_background(None)
-
- self._lastloc = Vector2(0, 0)
-
- def set_background(self, color: tuple[int, int, int] | None) -> None:
- """Set the background color for all rows in the pattern line."""
- self.set_attr_all("back", color)
- self.set_attr_all("image_update", True)
-
- def get_row(self, row: int) -> Row:
- """Return given row."""
- object_ = self.get_object(row)
- assert isinstance(object_, Row)
- return object_
-
- def reset_position(self) -> None:
- """Reset Locations of Rows according to self.location."""
- last = self.size[1]
- w = self.get_row(last - 1).width_height[0]
- if w is None:
- raise RuntimeError(
- "Image Dimensions for Row sprite.Sprite (row.width_height) are None!",
- )
- h1 = self.get_row(0).tile_full
- h = int(last * h1)
- self.width_height = w, h
- w1 = h1 / 2
-
- x, y = self.location
- y -= h / 2 - w1
- for rid in self.objects:
- row = self.get_row(rid)
- diff = last - row.size
- row.location = Vector2(x + (diff * w1), y + rid * h1)
-
- def get_tile_point(
- self,
- screen_location: tuple[int, int],
- ) -> tuple[int, int] | None:
- """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections."""
- for y in range(self.size[1]):
- x = self.get_row(y).get_tile_point(screen_location)
- if x is not None:
- return x, y
- return None
-
- def is_full(self) -> bool:
- """Return True if self is full."""
- return all(self.get_row(rid).is_full() for rid in range(self.size[1]))
-
- def wall_tiling(self) -> dict[str, list[int] | int | None]:
- """Return a dictionary to be used with wall tiling. Removes tiles from rows."""
- values: dict[str, list[int] | int | None] = {}
- for rid in range(self.size[1]):
- self.get_row(rid).wall_tile(values)
- return values
-
- def process(self, time_passed: float) -> None:
- """Process all the rows that make up the pattern line."""
- if self.hidden != self._lasthidden:
- self.set_attr_all("image_update", True)
- super().process(time_passed)
-
-
class FloorLine(Row):
"""Represents a player's floor line."""
- size = 7
- number_one_color = Tile.one
+ __slots__ = ("floor_line_id", "numbers", "text")
- def __init__(self, player: Player) -> None:
+ def __init__(self, floor_line_id: int) -> None:
"""Initialize floor line."""
- super().__init__(player, self.size, background=ORANGE)
- self.name = "floor_line"
+ super().__init__(f"floor_line_{floor_line_id}", 7, background=ORANGE)
# self.font = Font(FONT, round(self.tile_size*1.2), color=BLACK, cx=False, cy=False)
self.text = objects.Text(
@@ -1468,7 +844,6 @@ def __init__(self, player: Player) -> None:
cx=False,
cy=False,
)
- self.has_number_one_tile = False
self.numbers = [-255 for _ in range(self.size)]
@@ -1493,47 +868,6 @@ def render(self, surface: pygame.surface.Surface) -> None:
self.text.location = Vector2(*xy)
self.text.render(surface)
- # self.font.render(surface, str(self.numbers[x]), xy)
-
- def place_tile(self, tile: int) -> None:
- """Place a given int sprite.Sprite on self if permitted."""
- self.tiles.insert(self.get_placed(), tile)
-
- def score_tiles(self) -> int:
- """Score self.tiles and return how to change points."""
- running_total = 0
- for x in range(self.size):
- if self.tiles[x].color >= 0:
- running_total += self.numbers[x]
- elif x < self.size - 1 and self.tiles[x + 1].color >= 0:
- raise RuntimeError(
- "Player is likely cheating! Invalid placement of floor_line tiles!",
- )
- return running_total
-
- def get_tiles(
- self,
- empty_color: int = Tile.blank,
- ) -> tuple[list[int], int | None]:
- """Return tuple of tiles gathered, and then either the number one tile or None."""
- tiles = []
- number_one_tile = None
- for tile in (self.tiles.pop() for i in range(len(self.tiles))):
- if tile.color == self.number_one_color:
- number_one_tile = tile
- self.has_number_one_tile = False
- elif tile.color >= 0:
- tiles.append(tile)
-
- for _i in range(self.size):
- self.tiles.append(int(empty_color))
- self.image_update = True
- return tiles, number_one_tile
-
- def can_place_tiles(self, tiles: list[int]) -> bool:
- """Return True."""
- return True
-
class Factory(Grid):
"""Represents a Factory."""
@@ -1589,95 +923,6 @@ def get_tile_point(
)
-class Factories(MultipartObject):
- """Factories Multipart sprite.Sprite, made of multiple Factory Objects."""
-
- tiles_each = 4
-
- def __init__(
- self,
- game: Game,
- factories: int,
- size: int | None = None,
- ) -> None:
- """Initialize factories."""
- super().__init__("Factories")
-
- self.game = game
- self.count = factories
-
- for i in range(self.count):
- self.add_object(Factory(self.game, i))
-
- if size is None:
- factory = self.objects[0]
- assert isinstance(factory, Factory)
- factory.process(0)
- rad = factory.radius
- self.size = rad * 5
- else:
- self.size = size
- self.size = math.ceil(self.size)
-
- self.play_tiles_from_bag()
-
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"{self.__class__.__name__}(%r, %i, ...)" % (
- self.game,
- self.count,
- )
-
- def reset_position(self) -> None:
- """Reset the position of all factories within."""
- for index, degrees in enumerate(range(0, 360, 360 // self.count)):
- self.objects[index].location = (
- Vector2.from_degrees(
- degrees,
- 29 * 5,
- )
- + self.location
- )
-
- def process(self, time_passed: float) -> None:
- """Process factories. Does not react to cursor if hidden."""
- super().process(time_passed)
- if self.hidden:
- return
- cursor = self.game.get_object_by_name("Cursor")
- assert isinstance(cursor, Cursor)
- if not cursor.is_pressed() or cursor.is_holding():
- return
- obj, point = self.get_intersection(cursor.location)
- if obj is None or point is None:
- return
- oid = int(obj[7:])
-
- factory = self.objects[oid]
- assert isinstance(factory, Factory)
-
- tile_at_point = factory.get_info(point)
- if tile_at_point is None or tile_at_point.color < 0:
- return
- table = self.game.get_object_by_name("TableCenter")
- assert isinstance(table, TableCenter)
- select, tocenter = factory.grab_color(
- tile_at_point.color,
- )
- if tocenter:
- table.add_tiles(tocenter)
- cursor.drag(select)
-
- def is_all_empty(self) -> bool:
- """Return True if all factories are empty."""
- for fid in range(self.count):
- factory = self.objects[fid]
- assert isinstance(factory, Factory)
- if not factory.is_empty():
- return False
- return True
-
-
class TableCenter(TileRenderer):
"""sprite.Sprite that represents the center of the table."""
@@ -1745,356 +990,6 @@ def pull_tiles(self, tile_color: int) -> list[int]:
return [tile_color] * tile_count
-## def process(self, time_passed: float) -> None:
-## """Process factories."""
-## if self.hidden:
-## super().process(time_passed)
-## return
-## cursor = self.game.get_object_by_name("Cursor")
-## assert isinstance(cursor, Cursor)
-## if (
-## cursor.is_pressed()
-## and not cursor.is_holding()
-## and not self.is_empty()
-## and self.is_selected(cursor.location)
-## ):
-## point = self.get_tile_point(cursor.location)
-## # Shouldn't return none anymore since we have is_selected now.
-## assert point is not None
-## tile = self.get_info(point)
-## assert isinstance(tile, int)
-## color_at_point = tile.color
-## if color_at_point >= 0 and color_at_point < 5:
-## cursor.drag(self.pull_tiles(color_at_point))
-## super().process(time_passed)
-
-
-class Player(sprite.Sprite):
- """Represents a player. Made of lots of objects."""
-
- def __init__(
- self,
- game: Game,
- player_id: int,
- networked: bool = False,
- varient_play: bool = False,
- ) -> None:
- """Initialize player."""
- super().__init__(f"Player{player_id}")
-
- self.game = game
- self.player_id = player_id
- self.networked = networked
- self.varient_play = varient_play
-
- self.add_object(Board(self.varient_play))
- self.add_object(PatternLine(self))
- self.add_object(FloorLine(self))
- ## self.add_object(objects.objects.Text(SCOREFONTSIZE, SCORECOLOR))
-
- self.score = 0
- self.is_turn = False
- self.is_wall_tiling = False
- self.just_held = False
- self.just_dropped = False
-
- self.update_score()
-
- self._lastloc = Vector2(0, 0)
-
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"{self.__class__.__name__}(%r, %i, %s, %s)" % (
- self.game,
- self.player_id,
- self.networked,
- self.varient_play,
- )
-
- ## def update_score(self) -> None:
- ## """Update the scorebox for this player."""
- ## score_box = self.get_object_by_name("objects.Text")
- ## assert isinstance(score_box, objects.Text)
- ## score_box.update_value(f"Player {self.player_id + 1}: {self.score}")
-
- def trigger_turn_now(self) -> None:
- """Handle start of turn."""
- if not self.is_turn:
- pattern_line = self.get_object_by_name("PatternLine")
- assert isinstance(pattern_line, PatternLine)
- if self.is_wall_tiling:
- board = self.get_object_by_name("Board")
- assert isinstance(board, Board)
- rows = board.get_rows_to_tile_map()
- for rowpos, value in rows.items():
- color = get_tile_color(value, board.greyshift)
- assert isinstance(color[0], int)
- pattern_line.get_row(rowpos).set_background(
- color,
- )
- else:
- pattern_line.set_background(PATSELECTCOLOR)
- self.is_turn = True
-
- def end_of_turn(self) -> None:
- """Handle end of turn."""
- if self.is_turn:
- pattern_line = self.get_object_by_name("PatternLine")
- assert isinstance(pattern_line, PatternLine)
- pattern_line.set_background(None)
- self.is_turn = False
-
- ## def end_of_game_trigger(self) -> None:
- ## """Handle end of game.
- ##
- ## Called by end state when game is over
- ## Hide pattern lines and floor line.
- ## """
- ## pattern = self.get_object_by_name("PatternLine")
- ## floor = self.get_object_by_name("floor_line")
- ##
- ## pattern.hidden = True
- ## floor.hidden = True
-
- def reset_position(self) -> None:
- """Reset positions of all parts of self based off self.location."""
- x, y = self.location
-
- board = self.get_object_by_name("Board")
- assert isinstance(board, Board)
- bw, bh = board.width_height
- board.location = Vector2(x + bw // 2, y)
-
- pattern_line = self.get_object_by_name("PatternLine")
- assert isinstance(pattern_line, PatternLine)
- lw = pattern_line.width_height[0] // 2
- pattern_line.location = Vector2(x - lw, y)
-
- floor_line = self.get_object_by_name("floor_line")
- assert isinstance(floor_line, FloorLine)
- floor_line.location = Vector2(
- int(x - lw * (2 / 3) + TILESIZE / 3.75),
- int(y + bh * (2 / 3)),
- )
-
- text = self.get_object_by_name("objects.Text")
- assert isinstance(text, objects.Text)
- text.location = Vector2(x - (bw // 3), y - (bh * 2 // 3))
-
-
-## def wall_tiling(self) -> None:
-## """Do the wall tiling phase of the game for this player."""
-## self.is_wall_tiling = True
-## pattern_line = self.get_object_by_name("PatternLine")
-## assert isinstance(pattern_line, PatternLine)
-## board = self.get_object_by_name("Board")
-## assert isinstance(board, Board)
-## box_lid = self.game.get_object_by_name("BoxLid")
-## assert isinstance(box_lid, BoxLid)
-##
-## data = pattern_line.wall_tiling()
-## tiles_for_box = data["tiles_for_box"]
-## assert isinstance(tiles_for_box, list)
-## box_lid.add_tiles(tiles_for_box)
-## del data["tiles_for_box"]
-##
-## cleaned = {}
-## for key, value in data.items():
-## if not isinstance(value, int):
-## continue
-## cleaned[int(key)] = value
-##
-## board.wall_tiling_mode(cleaned)
-
-## def done_wall_tiling(self) -> bool:
-## """Return True if internal Board is done wall tiling."""
-## board = self.get_object_by_name("Board")
-## assert isinstance(board, Board)
-## return not board.is_wall_tiling()
-
-## def next_round(self) -> None:
-## """Handle end of wall tiling."""
-## self.is_wall_tiling = False
-
-## def score_phase(self) -> int | None:
-## """Do the scoring phase of the game for this player. Return number one tile or None."""
-## board = self.get_object_by_name("Board")
-## floor_line = self.get_object_by_name("floor_line")
-## box_lid = self.game.get_object_by_name("BoxLid")
-## assert isinstance(board, Board)
-## assert isinstance(floor_line, FloorLine)
-## assert isinstance(box_lid, BoxLid)
-##
-## def saturatescore() -> None:
-## if self.score < 0:
-## self.score = 0
-##
-## self.score += board.score_additions()
-## self.score += floor_line.score_tiles()
-## saturatescore()
-##
-## tiles_for_box, number_one = floor_line.get_tiles()
-## box_lid.add_tiles(tiles_for_box)
-##
-## self.update_score()
-##
-## return number_one
-
-## def end_of_game_scoring(self) -> None:
-## """Update final score with additional end of game points."""
-## board = self.get_object_by_name("Board")
-## assert isinstance(board, Board)
-##
-## self.score += board.end_of_game_scoreing()
-##
-## self.update_score()
-
-## def has_horzontal_line(self) -> bool:
-## """Return True if this player has a horizontal line on their game board filled."""
-## board = self.get_object_by_name("Board")
-## assert isinstance(board, Board)
-##
-## return board.has_filled_row()
-
-## def get_horizontal_lines(self) -> int:
-## """Return the number of filled horizontal lines this player has on their game board."""
-## board = self.get_object_by_name("Board")
-## assert isinstance(board, Board)
-##
-## return board.get_filled_rows()
-
-## def process(self, time_passed: float) -> None:
-## """Process Player."""
-## if not self.is_turn: # Is our turn?
-## self.set_attr_all("hidden", self.hidden)
-## super().process(time_passed)
-## return
-## if self.hidden and self.is_wall_tiling and self.varient_play:
-## # If hidden, not anymore. Our turn.
-## self.hidden = False
-## if self.networked: # We are networked.
-## self.set_attr_all("hidden", self.hidden)
-## super().process(time_passed)
-## return
-##
-## cursor = self.game.get_object_by_name("Cursor")
-## assert isinstance(cursor, Cursor)
-## box_lid = self.game.get_object_by_name("BoxLid")
-## assert isinstance(box_lid, BoxLid)
-## pattern_line = self.get_object_by_name("PatternLine")
-## assert isinstance(pattern_line, PatternLine)
-## floor_line = self.get_object_by_name("floor_line")
-## assert isinstance(floor_line, FloorLine)
-## board = self.get_object_by_name("Board")
-## assert isinstance(board, Board)
-##
-## if not cursor.is_pressed():
-## # Mouse up
-## if self.just_held:
-## self.just_held = False
-## if self.just_dropped:
-## self.just_dropped = False
-## self.set_attr_all("hidden", self.hidden)
-## super().process(time_passed)
-## return
-##
-## # Mouse down
-## obj, point = self.get_intersection(cursor.location)
-## if obj is None or point is None:
-## if self.is_wall_tiling and self.done_wall_tiling():
-## self.next_round()
-## self.game.next_turn()
-## self.set_attr_all("hidden", self.hidden)
-## super().process(time_passed)
-## return
-## # Something pressed
-## if cursor.is_holding(): # Cursor holding tiles
-## move_made = False
-## if not self.is_wall_tiling: # Is wall tiling:
-## if obj == "PatternLine":
-## pos, row_number = point
-## row = pattern_line.get_row(row_number)
-## if not row.is_full():
-## info = row.get_info(pos)
-## if info is not None and info.color < 0:
-## _color, _held = cursor.get_held_info()
-## todrop = min(
-## pos + 1,
-## row.get_placeable(),
-## )
-## tiles = cursor.drop(todrop)
-## if row.can_place_tiles(tiles):
-## row.place_tiles(tiles)
-## move_made = True
-## else:
-## cursor.force_hold(tiles)
-## elif obj == "floor_line":
-## tiles_to_add = cursor.drop()
-## if floor_line.is_full():
-## # Floor is full,
-## # Add tiles to box instead.
-## box_lid.add_tiles(tiles_to_add)
-## elif floor_line.get_placeable() < len(
-## tiles_to_add,
-## ):
-## # Floor is not full but cannot fit all in floor line.
-## # Add tiles to floor line and then to box
-## while len(tiles_to_add) > 0:
-## if floor_line.get_placeable() > 0:
-## floor_line.place_tile(
-## tiles_to_add.pop(),
-## )
-## else:
-## box_lid.add_tile(
-## tiles_to_add.pop(),
-## )
-## else:
-## # Otherwise add to floor line for all.
-## floor_line.place_tiles(tiles_to_add)
-## move_made = True
-## elif not self.just_held and obj == "Board":
-## tile = board.get_info(point)
-## assert isinstance(tile, int)
-## if tile.color == Tile.blank:
-## # Cursor holding and wall tiling
-## _column, row_id = point
-## cursor_tile = cursor.drop(1)[0]
-## board_tile = board.get_tile_for_cursor_by_row(
-## row_id,
-## )
-## if (
-## board_tile is not None
-## and cursor_tile.color == board_tile.color
-## and board.wall_tile_from_point(point)
-## ):
-## self.just_dropped = True
-## pattern_line.get_row(
-## row_id,
-## ).set_background(None)
-## if move_made and not self.is_wall_tiling:
-## if cursor.holding_number_one:
-## one_tile = cursor.drop_one_tile()
-## assert one_tile is not None
-## floor_line.place_tile(one_tile)
-## if cursor.get_held_count(True) == 0:
-## self.game.next_turn()
-## elif self.is_wall_tiling and obj == "Board" and not self.just_dropped:
-## # Mouse down, something pressed, and not holding anything
-## # Wall tiling, pressed, not holding
-## _column_number, row_number = point
-## tile = board.get_tile_for_cursor_by_row(
-## row_number,
-## )
-## if tile is not None:
-## cursor.drag([tile])
-## self.just_held = True
-## if self.is_wall_tiling and self.done_wall_tiling():
-## self.next_round()
-## self.game.next_turn()
-## self.set_attr_all("hidden", self.hidden)
-## super().process(time_passed)
-
-
class HaltState(AsyncState["AzulClient"]):
"""Halt state to set state to None so running becomes False."""
@@ -2413,7 +1308,7 @@ async def entry_actions(self) -> None:
0,
hosting_button.rect.h + 10,
),
- handle_click=self.change_state("play_internal_hosting"),
+ handle_click=self.change_state("play_hosting_internal"),
)
self.group_add(internal_button)
@@ -2592,28 +1487,6 @@ def __init__(self) -> None:
"""Initialize factory offer phase."""
super().__init__("FactoryOffer")
- def entry_actions(self) -> None:
- """Advance turn."""
- assert self.game is not None
- self.game.next_turn()
-
- def check_state(self) -> str | None:
- """If all tiles are gone, go to wall tiling. Otherwise keep waiting for that to happen."""
- assert self.game is not None
- fact = self.game.get_object_by_name("Factories")
- assert isinstance(fact, Factories)
- table = self.game.get_object_by_name("TableCenter")
- assert isinstance(table, TableCenter)
- cursor = self.game.get_object_by_name("Cursor")
- assert isinstance(cursor, Cursor)
- if (
- fact.is_all_empty()
- and table.is_empty()
- and not cursor.is_holding(True)
- ):
- return "WallTiling"
- return None
-
class PhaseFactoryOfferNetworked(PhaseFactoryOffer):
"""Factory offer phase but networked."""
@@ -2722,15 +1595,6 @@ def entry_actions(self) -> None:
complete = (player.has_horzontal_line() for player in players)
self.new_round = not any(complete)
- def do_actions(self) -> None:
- """Perform actions of state."""
- assert self.game is not None
- if self.new_round:
- fact = self.game.get_object_by_name("Factories")
- assert isinstance(fact, Factories)
- # This also handles bag re-filling from box lid.
- fact.play_tiles_from_bag()
-
def check_state(self) -> str:
"""Go to factory offer if new round else end screen."""
if self.new_round:
@@ -2741,265 +1605,11 @@ def check_state(self) -> str:
class EndScreen(MenuState):
"""End screen state."""
+ __slots__ = ()
+
def __init__(self) -> None:
"""Initialize end screen."""
super().__init__("End")
- self.ranking: dict[int, list[int]] = {}
- self.wininf = ""
-
- def get_winners(self) -> None:
- """Update self.ranking by player scores."""
- assert self.game is not None
- self.ranking.clear()
- scpid = {}
- for player_id in range(self.game.players):
- player = self.game.get_player(player_id)
- assert isinstance(player, Player)
- player.end_of_game_trigger()
- if player.score not in scpid:
- scpid[player.score] = [player_id]
- else:
- scpid[player.score] += [player_id]
- # make sure no ties and establish rank
- rank = 1
- for score in sorted(scpid, reverse=True):
- pids = scpid[score]
- if len(pids) > 1:
- # If players have same score,
- # most horizontal lines is tie breaker.
- players = [
- self.game.get_player(player_id) for player_id in pids
- ]
- lines = [
- (p.get_horizontal_lines(), p.player_id) for p in players
- ]
- last = None
- for c, player_id in sorted(
- lines,
- key=operator.itemgetter(0),
- reverse=True,
- ):
- if last == c:
- self.ranking[rank - 1] += [player_id + 1]
- continue
- last = c
- self.ranking[rank] = [player_id + 1]
- rank += 1
- else:
- self.ranking[rank] = [pids[0] + 1]
- rank += 1
- # Finally, make nice text.
- text = ""
- for rank in sorted(self.ranking):
- line = "Player"
- players_rank = self.ranking[rank]
- cnt = len(players_rank)
- if cnt > 1:
- line += "s"
- line += " "
- if cnt == 1:
- line += "{}"
- elif cnt == 2:
- line += "{} and {}"
- elif cnt >= 3:
- tmp = (["{}"] * (cnt - 1)) + ["and {}"]
- line += ", ".join(tmp)
- line += " "
- if cnt == 1:
- line += "got"
- else:
- line += "tied for"
- line += " "
- if rank <= 2:
- line += ("1st", "2nd")[rank - 1]
- else:
- line += f"{rank}th"
- line += " place!\n"
- text += line.format(*players_rank)
- self.wininf = text[:-1]
-
- def entry_actions(self) -> None:
- """Set up end screen."""
- assert self.game is not None
- # Figure out who won the game by points.
- self.get_winners()
- # Hide everything
- table = self.game.get_object_by_name("TableCenter")
- assert isinstance(table, TableCenter)
- table.hidden = True
-
- fact = self.game.get_object_by_name("Factories")
- assert isinstance(fact, Factories)
- fact.set_attr_all("hidden", True)
-
- # Add buttons
- bid = self.add_button(
- "ReturnTitle",
- "Return to Title",
- self.to_state("Title"),
- (SCREEN_SIZE[0] // 2, SCREEN_SIZE[1] * 4 // 5),
- )
- buttontitle = self.game.get_object(bid)
- assert isinstance(buttontitle, objects.Button)
- buttontitle.Render_Priority = "last-1"
- buttontitle.cur_time = 2
-
- # Add score board
- x = SCREEN_SIZE[0] // 2
- y = 10
- for idx, line in enumerate(self.wininf.split("\n")):
- self.add_text(f"Line{idx}", line, (x, y), cx=True, cy=False)
- # self.game.get_object(bid).Render_Priority = f'last{-(2+idx)}'
- button = self.game.get_object(bid)
- assert isinstance(button, objects.Button)
- button.Render_Priority = "last-2"
- y += self.bh
-
-
-class Game(ObjectHandler):
- """Game object, contains most of what's required for Azul."""
-
- tile_size = 30
-
- def __init__(self) -> None:
- """Initialize game."""
- super().__init__()
-
- self.states: dict[str, GameState] = {}
- self.active_state: GameState | None = None
-
- self.add_states(
- [
- InitializeState(),
- TitleState(),
- CreditsState(),
- SettingsState(),
- PhaseFactoryOffer(),
- PhaseWallTiling(),
- PhasePrepareNext(),
- EndScreen(),
- ## PhaseFactoryOfferNetworked(),
- ## PhaseWallTilingNetworked(),
- ## PhasePrepareNextNetworked(),
- ## EndScreenNetworked(),
- ],
- )
- self.initialized_state = False
-
- self.background_color = BACKGROUND
-
- self.is_host = True
- self.players = 0
- self.factories = 0
-
- self.player_turn: int = 0
-
- # # Cache
- # self.cache: dict[int, pygame.surface.Surface] = {}
-
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"{self.__class__.__name__}()"
-
- def add_object(self, obj: sprite.Sprite) -> None:
- """Add an object to the game."""
- obj.game = self
- super().add_object(obj)
-
- def get_player(self, player_id: int) -> Player:
- """Get the player with player id player_id."""
- if self.players:
- player = self.get_object_by_name(f"Player{player_id}")
- assert isinstance(player, Player)
- return player
- raise RuntimeError("No players!")
-
- def player_turn_over(self) -> None:
- """Call end_of_turn for current player."""
- if self.player_turn >= 0 and self.player_turn < self.players:
- old_player = self.get_player(self.player_turn)
- if old_player.is_turn:
- old_player.end_of_turn()
-
- def next_turn(self) -> None:
- """Tell current player it's the end of their turn, and update who's turn it is and now it's their turn."""
- if self.is_host:
- self.player_turn_over()
- last = self.player_turn
- self.player_turn = (self.player_turn + 1) % self.players
- if self.player_turn == last and self.players > 1:
- self.next_turn()
- return
- new_player = self.get_player(self.player_turn)
- new_player.trigger_turn_now()
-
- def start_game(
- self,
- players: int,
- varient_play: bool = False,
- host_mode: bool = True,
- address: str = "",
- ) -> None:
- """Start a new game."""
- self.reset_cache()
- max_players = 4
- self.players = saturate(players, 1, max_players)
- self.is_host = host_mode
- self.factories = self.players * 2 + 1
-
- self.rm_star()
-
- self.add_object(Cursor(self))
- self.add_object(TableCenter(self))
-
- if self.is_host:
- self.bag.reset()
- # S311 Standard pseudo-random generators are not suitable for cryptographic purposes
- self.player_turn = random.randint( # noqa: S311
- -1,
- self.players - 1,
- )
- else:
- raise NotImplementedError()
-
- cx, cy = SCREEN_SIZE[0] / 2, SCREEN_SIZE[1] / 2
- out = math.sqrt(cx**2 + cy**2) // 3 * 2
-
- mdeg = 360 // max_players
-
- for player_id in range(self.players):
- networked = False
- newp = Player(self, player_id, networked, varient_play)
-
- truedeg = (self.players + 1 - player_id) * (360 / self.players)
- closedeg = truedeg // mdeg * mdeg + 45
- rad = math.radians(closedeg)
-
- newp.location = Vector2(
- round(cx + out * math.sin(rad)),
- round(
- cy + out * math.cos(rad),
- ),
- )
- self.add_object(newp)
- if self.is_host:
- self.next_turn()
-
- factory = Factories(self, self.factories)
- factory.location = Vector2(cx, cy)
- self.add_object(factory)
- self.process_objects(0)
-
- if self.is_host:
- self.next_turn()
-
- def screen_size_update(self) -> None:
- """Handle screen size updates."""
- objs_with_attr = self.get_objects_with_attr("screen_size_update")
- for oid in objs_with_attr:
- obj = self.get_object(oid)
- assert obj is not None
- obj.screen_size_update()
class PlayHostingState(AsyncState["AzulClient"]):
@@ -3012,7 +1622,7 @@ class PlayHostingState(AsyncState["AzulClient"]):
def __init__(self) -> None:
"""Initialize Play internal hosting / hosting State."""
extra = "_internal" if self.internal_server else ""
- super().__init__(f"play{extra}_hosting")
+ super().__init__(f"play_hosting{extra}")
async def entry_actions(self) -> None:
"""Start hosting server."""
@@ -3299,7 +1909,7 @@ async def handle_game_initial_config(
# Add players
# TODO: Do it properly
for index, degrees in enumerate(range(0, 360, 360 // player_count)):
- board = Board(f"Board_{index}", varient_play)
+ board = Board(index)
board.location = (
Vector2.from_degrees(
degrees - 45,
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index 862ff9f..30f41ee 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -26,11 +26,15 @@
from collections import Counter
from enum import IntEnum, auto
-from typing import Final, TypeAlias
+from typing import TYPE_CHECKING, Final, TypeAlias
from libcomponent.base_io import StructFormat
from libcomponent.buffer import Buffer
from mypy_extensions import u8
+from numpy import int8, zeros
+
+if TYPE_CHECKING:
+ from numpy.typing import NDArray
ADVERTISEMENT_IP: Final = "224.0.2.60"
ADVERTISEMENT_PORT: Final = 4445
@@ -47,6 +51,7 @@ def encode_numeric_uint8_counter(counter: Counter[int]) -> Buffer:
for key, value in counter.items():
assert isinstance(key, int)
buffer.write_value(StructFormat.UBYTE, key)
+ assert value >= 0
buffer.write_value(StructFormat.UBYTE, value)
return buffer
@@ -64,6 +69,26 @@ def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[int]:
return Counter(data)
+def encode_int8_array(array: NDArray[int8]) -> Buffer:
+ """Return buffer from int8 array flat values."""
+ buffer = Buffer()
+
+ for value in array.flat:
+ buffer.write_value(StructFormat.BYTE, value)
+
+ return buffer
+
+
+def decode_int8_array(buffer: Buffer, size: tuple[int, ...]) -> NDArray[int8]:
+ """Return flattened int8 array from buffer."""
+ array = zeros(size, dtype=int8)
+
+ for index in range(array.size):
+ array.flat[index] = buffer.read_value(StructFormat.BYTE)
+
+ return array
+
+
class ClientBoundEvents(IntEnum):
"""Client bound event IDs."""
@@ -72,6 +97,7 @@ class ClientBoundEvents(IntEnum):
initial_config = auto()
playing_as = auto()
game_over = auto()
+ board_data = auto()
class ServerBoundEvents(IntEnum):
diff --git a/src/azul/server.py b/src/azul/server.py
index 13a9397..ab78f9a 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
-# Checkers Game Server
+# Azul Game Server
-"""Checkers Game Server."""
+"""Azul Game Server."""
# Programmed by CoolCat467
@@ -27,7 +27,6 @@
__license__ = "GNU General Public License Version 3"
__version__ = "0.0.0"
-import time
import traceback
from collections import deque
from functools import partial
@@ -53,12 +52,16 @@
DEFAULT_PORT,
ClientBoundEvents,
ServerBoundEvents,
+ encode_int8_array,
)
from azul.state import Phase, State
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
+ from numpy import int8
+ from numpy.typing import NDArray
+
# cursor_set_movement_mode
# cursor_set_destination
@@ -87,8 +90,9 @@ def __init__(self, client_id: int) -> None:
"server[write]->encryption_request": cbe.encryption_request,
"server[write]->callback_ping": cbe.callback_ping,
"server[write]->initial_config": cbe.initial_config,
- "server[write]->game_over": cbe.game_over,
"server[write]->playing_as": cbe.playing_as,
+ "server[write]->game_over": cbe.game_over,
+ "server[write]->board_data": cbe.board_data,
},
)
sbe = ServerBoundEvents
@@ -103,23 +107,32 @@ def bind_handlers(self) -> None:
super().bind_handlers()
self.register_handlers(
{
- f"callback_ping->network[{self.client_id}]": self.handle_callback_ping,
f"client[{self.client_id}]->encryption_response": self.handle_encryption_response,
+ f"callback_ping->network[{self.client_id}]": self.handle_callback_ping,
"initial_config->network": self.handle_initial_config,
- "game_over->network": self.handle_game_over,
f"playing_as->network[{self.client_id}]": self.handle_playing_as,
+ "game_over->network": self.handle_game_over,
+ "board_data->network": self.handle_board_data,
},
)
- async def handle_game_over(self, event: Event[int]) -> None:
- """Read game over event and reraise as server[write]->game_over."""
- winner = event.data
-
- buffer = Buffer()
+ async def start_encryption_request(self) -> None:
+ """Start encryption request and raise as `server[write]->encryption_request`."""
+ await super().start_encryption_request()
- buffer.write_value(StructFormat.UBYTE, winner)
+ event = await self.read_event()
+ if event.name != f"client[{self.client_id}]->encryption_response":
+ raise RuntimeError(
+ f"Expected encryption response, got but {event.name!r}",
+ )
+ await self.handle_encryption_response(event)
- await self.write_event(Event("server[write]->game_over", buffer))
+ async def handle_callback_ping(
+ self,
+ _: Event[None],
+ ) -> None:
+ """Reraise as server[write]->callback_ping."""
+ await self.write_callback_ping()
async def handle_initial_config(
self,
@@ -148,48 +161,32 @@ async def handle_playing_as(
buffer.write_value(StructFormat.UBYTE, playing_as)
await self.write_event(Event("server[write]->playing_as", buffer))
- async def write_callback_ping(self) -> None:
- """Write callback_ping packet to client.
-
- Could raise the following exceptions:
- trio.BrokenResourceError: if something has gone wrong, and the stream
- is broken.
- trio.ClosedResourceError: if stream was previously closed
+ async def handle_game_over(self, event: Event[int]) -> None:
+ """Read game over event and reraise as server[write]->game_over."""
+ winner = event.data
- Listed as possible but probably not because of write lock:
- trio.BusyResourceError: if another task is using :meth:`write`
- """
buffer = Buffer()
- # Try to be as accurate with time as possible
- await self.wait_write_might_not_block()
- ns = int(time.time() * 1e9)
- # Use as many bits as time needs, write_buffer handles size for us.
- buffer.write(ns.to_bytes(-(-ns.bit_length() // 8), "big"))
+ buffer.write_value(StructFormat.UBYTE, winner)
- await self.write_event(Event("server[write]->callback_ping", buffer))
+ await self.write_event(Event("server[write]->game_over", buffer))
- async def handle_callback_ping(
+ async def handle_board_data(
self,
- _: Event[None],
+ event: Event[tuple[int, NDArray[int8]]],
) -> None:
- """Reraise as server[write]->callback_ping."""
- await self.write_callback_ping()
+ """Reraise as server[write]->board_data."""
+ player_id, array = event.data
- async def start_encryption_request(self) -> None:
- """Start encryption request and raise as `server[write]->encryption_request`."""
- await super().start_encryption_request()
+ buffer = Buffer()
+ buffer.write_value(StructFormat.UBYTE, player_id)
+ buffer.extend(encode_int8_array(array))
- event = await self.read_event()
- if event.name != f"client[{self.client_id}]->encryption_response":
- raise RuntimeError(
- f"Expected encryption response, got but {event.name!r}",
- )
- await self.handle_encryption_response(event)
+ await self.write_event(Event("server[write]->board_data", buffer))
class GameServer(network.Server):
- """Checkers server.
+ """Azul server.
Handles accepting incoming connections from clients and handles
main game logic via State subclass above.
@@ -213,7 +210,7 @@ def __init__(self, internal_singleplayer_mode: bool = False) -> None:
super().__init__("GameServer")
self.client_count: int = 0
- self.state = State.new_game(0)
+ self.state = State.blank()
self.client_players: dict[int, int] = {}
self.players_can_interact: bool = False
@@ -410,8 +407,6 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
## Event("create_piece->network", (piece_pos, piece_type)),
## )
- await self.transmit_playing_as()
-
# Raise initial config event with board size and initial turn.
await self.raise_event(
Event(
@@ -425,6 +420,24 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
),
)
+ print(f"{self.state.player_data = }")
+
+ # Transmit board data for all players
+ async with trio.open_nursery() as nursery:
+ for player_id, player_data in self.state.player_data.items():
+ nursery.start_soon(
+ self.raise_event,
+ Event(
+ "board_data->network",
+ (
+ player_id,
+ player_data.wall,
+ ),
+ ),
+ )
+
+ await self.transmit_playing_as()
+
async def client_network_loop(
self,
client: ServerClient,
@@ -555,7 +568,8 @@ async def handler(self, stream: trio.SocketStream) -> None:
assert client.encryption_enabled
if can_start and game_active:
- await self.send_spectator_join_packets(client)
+ print("TODO: Joined as spectator")
+ # await self.send_spectator_join_packets(client)
with self.temporary_component(client):
if can_start and not game_active and is_zee_capitan:
varient_play = False
diff --git a/src/azul/state.py b/src/azul/state.py
index 7439967..755bc9b 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -649,6 +649,20 @@ class State(NamedTuple):
current_turn: int
player_data: dict[int, PlayerData]
+ @classmethod
+ def blank(cls) -> Self:
+ return cls(
+ varient_play=False,
+ current_phase=Phase.end,
+ bag=Counter(),
+ box_lid=Counter(),
+ table_center=Counter(),
+ factory_displays={},
+ cursor_contents=Counter(),
+ current_turn=0,
+ player_data={},
+ )
+
@classmethod
def new_game(cls, player_count: int, varient_play: bool = False) -> Self:
"""Return state of a new game."""
From 35bf6fdee6767662c4fd6947eb3d24b94b698ac7 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 29 Nov 2024 16:37:27 -0600
Subject: [PATCH 10/67] Send factory display tile data over the network
---
src/azul/client.py | 14 ++++++++-
src/azul/game.py | 61 ++++++++++++++++++++++++++------------
src/azul/network_shared.py | 36 +++++++++++++++++-----
src/azul/server.py | 36 ++++++++++++++++++++--
4 files changed, 116 insertions(+), 31 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index b9a1d1a..64ac3f4 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -41,6 +41,7 @@
ClientBoundEvents,
ServerBoundEvents,
decode_int8_array,
+ decode_numeric_uint8_counter,
)
if TYPE_CHECKING:
@@ -175,6 +176,7 @@ def __init__(self, name: str) -> None:
cbe.playing_as: "server->playing_as",
cbe.game_over: "server->game_over",
cbe.board_data: "server->board_data",
+ cbe.factory_data: "server->factory_data",
},
)
@@ -192,6 +194,7 @@ def bind_handlers(self) -> None:
"server->playing_as": self.read_playing_as,
"server->game_over": self.read_game_over,
"server->board_data": self.read_board_data,
+ "server->factory_data": self.read_factory_data,
"client_connect": self.handle_client_connect,
"network_stop": self.handle_network_stop,
},
@@ -351,7 +354,7 @@ async def read_game_over(self, event: Event[bytearray]) -> None:
self.running = False
async def read_board_data(self, event: Event[bytearray]) -> None:
- """Read board_data event from server."""
+ """Read board_data event from server, reraise as `game_board_data`."""
buffer = Buffer(event.data)
player_id: u8 = buffer.read_value(StructFormat.UBYTE)
@@ -359,6 +362,15 @@ async def read_board_data(self, event: Event[bytearray]) -> None:
await self.raise_event(Event("game_board_data", (player_id, array)))
+ async def read_factory_data(self, event: Event[bytearray]) -> None:
+ """Read factory_data event from server, reraise as `game_factory_data`."""
+ buffer = Buffer(event.data)
+
+ factory_id: u8 = buffer.read_value(StructFormat.UBYTE)
+ tiles = decode_numeric_uint8_counter(buffer)
+
+ await self.raise_event(Event("game_factory_data", (factory_id, tiles)))
+
async def handle_network_stop(self, event: Event[None]) -> None:
"""Send EOF if connected and close socket."""
if self.not_connected:
diff --git a/src/azul/game.py b/src/azul/game.py
index 59c288f..f26a9dc 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -869,32 +869,55 @@ def render(self, surface: pygame.surface.Surface) -> None:
self.text.render(surface)
-class Factory(Grid):
+class Factory(TileRenderer):
"""Represents a Factory."""
+ __slots__ = ("factory_id", "tiles")
color = WHITE
outline = BLUE
def __init__(self, factory_id: int) -> None:
"""Initialize factory."""
- super().__init__(f"Factory_{factory_id}", (2, 2), background=None)
+ super().__init__(f"Factory_{factory_id}", background=None)
- self.number = factory_id
+ self.factory_id = factory_id
+ self.tiles: Counter[int] = Counter()
- self.redraw()
- self.visible = True
+ self.update_location_on_resize = True
def __repr__(self) -> str:
"""Return representation of self."""
return f"{self.__class__.__name__}({self.number})"
- def clear_image(
+ def bind_handlers(self) -> None:
+ """Register event handlers."""
+ self.register_handlers(
+ {
+ "game_factory_data": self.handle_factory_data,
+ },
+ )
+
+ async def handle_factory_data(
self,
- tile_size: tuple[int, int],
- extra_space: tuple[int, int] | None,
+ event: Event[tuple[int, Counter[int]]],
) -> None:
- """Clear self.image and draw circles."""
- super().clear_image(tile_size, extra_space)
+ """Handle `game_factory_data` event."""
+ factory_id, tiles = event.data
+
+ if factory_id != self.factory_id:
+ await trio.lowlevel.checkpoint()
+ return
+
+ self.tiles = tiles
+ self.update_image()
+ self.visible = True
+
+ await trio.lowlevel.checkpoint()
+
+ def update_image(self) -> None:
+ """Update image."""
+ self.clear_image((2, 2), extra=(16, 16))
+
radius = 29
pygame.draw.circle(
self.image,
@@ -909,9 +932,9 @@ def clear_image(
math.ceil(radius * 0.9),
)
- def redraw(self) -> None:
- """Redraw this factory."""
- super().update_image(offset=(8, 8), extra_space=(16, 16))
+ for index, tile_color in enumerate(self.tiles.elements()):
+ y, x = divmod(index, 2)
+ self.blit_tile(tile_color, (x, y), (8, 8))
def get_tile_point(
self,
@@ -1839,12 +1862,14 @@ async def handle_return_to_title(self, _: Event[None]) -> None:
class PlayState(GameState):
"""Game Play State."""
- __slots__ = ("exit_data",)
+ __slots__ = ("current_turn", "exit_data")
def __init__(self) -> None:
"""Initialize Play State."""
super().__init__("play")
+ self.current_turn: int = 0
+
# (0: normal | 1: error) handled>
self.exit_data: tuple[int, str, bool] | None = None
@@ -1887,10 +1912,9 @@ async def handle_game_initial_config(
event: Event[tuple[bool, int, int, int]],
) -> None:
"""Handle `game_initial_config` event."""
- varient_play, player_count, factory_count, current_turn = event.data
-
- print("handle_game_initial_config")
- print((varient_play, player_count, factory_count, current_turn))
+ varient_play, player_count, factory_count, self.current_turn = (
+ event.data
+ )
center = Vector2.from_iter(SCREEN_SIZE) // 2
@@ -1907,7 +1931,6 @@ async def handle_game_initial_config(
self.group_add(factory)
# Add players
- # TODO: Do it properly
for index, degrees in enumerate(range(0, 360, 360 // player_count)):
board = Board(index)
board.location = (
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index 30f41ee..92adb3e 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -44,25 +44,44 @@
Pos: TypeAlias = tuple[u8, u8]
+def encode_tile_count(tile_color: int, tile_count: int) -> Buffer:
+ """Return buffer from tile color and count."""
+ buffer = Buffer()
+
+ buffer.write_value(StructFormat.UBYTE, tile_color)
+ buffer.write_value(StructFormat.UBYTE, tile_count)
+
+ return buffer
+
+
+def decode_tile_count(buffer: Buffer) -> tuple[int, int]:
+ """Read and return tile color and count from buffer."""
+ tile_color = buffer.read_value(StructFormat.UBYTE)
+ tile_count = buffer.read_value(StructFormat.UBYTE)
+
+ return (tile_color, tile_count)
+
+
def encode_numeric_uint8_counter(counter: Counter[int]) -> Buffer:
- """Return buffer from uint8 counter (both keys and values)."""
+ """Return buffer from uint8 counter."""
buffer = Buffer()
+ buffer.write_value(StructFormat.UBYTE, len(counter))
for key, value in counter.items():
assert isinstance(key, int)
- buffer.write_value(StructFormat.UBYTE, key)
assert value >= 0
- buffer.write_value(StructFormat.UBYTE, value)
+ buffer.extend(encode_tile_count(key, value))
+
return buffer
def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[int]:
- """Return buffer from uint8 counter (both keys and values)."""
+ """Read and return uint8 counter from buffer."""
data: dict[int, int] = {}
- for _ in range(0, len(buffer), 2):
- key = buffer.read_value(StructFormat.UBYTE)
- value = buffer.read_value(StructFormat.UBYTE)
+ pair_count = buffer.read_value(StructFormat.UBYTE)
+ for _ in range(pair_count):
+ key, value = decode_tile_count(buffer)
assert key not in data
data[key] = value
@@ -74,7 +93,7 @@ def encode_int8_array(array: NDArray[int8]) -> Buffer:
buffer = Buffer()
for value in array.flat:
- buffer.write_value(StructFormat.BYTE, value)
+ buffer.write_value(StructFormat.BYTE, int(value))
return buffer
@@ -98,6 +117,7 @@ class ClientBoundEvents(IntEnum):
playing_as = auto()
game_over = auto()
board_data = auto()
+ factory_data = auto()
class ServerBoundEvents(IntEnum):
diff --git a/src/azul/server.py b/src/azul/server.py
index ab78f9a..44adf47 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -53,10 +53,12 @@
ClientBoundEvents,
ServerBoundEvents,
encode_int8_array,
+ encode_numeric_uint8_counter,
)
from azul.state import Phase, State
if TYPE_CHECKING:
+ from collections import Counter
from collections.abc import Awaitable, Callable
from numpy import int8
@@ -93,6 +95,7 @@ def __init__(self, client_id: int) -> None:
"server[write]->playing_as": cbe.playing_as,
"server[write]->game_over": cbe.game_over,
"server[write]->board_data": cbe.board_data,
+ "server[write]->factory_data": cbe.factory_data,
},
)
sbe = ServerBoundEvents
@@ -113,6 +116,7 @@ def bind_handlers(self) -> None:
f"playing_as->network[{self.client_id}]": self.handle_playing_as,
"game_over->network": self.handle_game_over,
"board_data->network": self.handle_board_data,
+ "factory_data->network": self.handle_factory_data,
},
)
@@ -184,6 +188,19 @@ async def handle_board_data(
await self.write_event(Event("server[write]->board_data", buffer))
+ async def handle_factory_data(
+ self,
+ event: Event[tuple[int, Counter[int]]],
+ ) -> None:
+ """Reraise as server[write]->factory_data."""
+ factory_id, tiles = event.data
+
+ buffer = Buffer()
+ buffer.write_value(StructFormat.UBYTE, factory_id)
+ buffer.extend(encode_numeric_uint8_counter(tiles))
+
+ await self.write_event(Event("server[write]->factory_data", buffer))
+
class GameServer(network.Server):
"""Azul server.
@@ -420,10 +437,8 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
),
)
- print(f"{self.state.player_data = }")
-
- # Transmit board data for all players
async with trio.open_nursery() as nursery:
+ # Transmit board data
for player_id, player_data in self.state.player_data.items():
nursery.start_soon(
self.raise_event,
@@ -435,6 +450,21 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
),
),
)
+ # Transmit factory data
+ for (
+ factory_id,
+ factory_tiles,
+ ) in self.state.factory_displays.items():
+ nursery.start_soon(
+ self.raise_event,
+ Event(
+ "factory_data->network",
+ (
+ factory_id,
+ factory_tiles,
+ ),
+ ),
+ )
await self.transmit_playing_as()
From 086e0255dab3214b1cb76b9743e3ab1144a5c69f Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 29 Nov 2024 19:19:34 -0600
Subject: [PATCH 11/67] Add PatternRows and fix a few issues
---
src/azul/crop.py | 59 +++++-
src/azul/game.py | 419 ++++++++++++-------------------------
src/azul/mr_floppy_test.py | 2 +-
src/azul/sprite.py | 25 +--
4 files changed, 203 insertions(+), 302 deletions(-)
diff --git a/src/azul/crop.py b/src/azul/crop.py
index 9aeb132..cbdbbab 100644
--- a/src/azul/crop.py
+++ b/src/azul/crop.py
@@ -2,14 +2,37 @@
# Programmed by CoolCat467
+from __future__ import annotations
+
+# Copyright (C) 2020-2024 CoolCat467
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
__title__ = "Crop Functions"
__author__ = "CoolCat467"
__version__ = "0.0.0"
+
+from typing import TYPE_CHECKING
+
from pygame.color import Color
from pygame.rect import Rect
from pygame.surface import Surface
+if TYPE_CHECKING:
+ from collections.abc import Callable, Generator, Iterable
+
def crop_color(surface: Surface, color: Color) -> Surface:
"""Crop out color from surface."""
@@ -46,10 +69,40 @@ def crop_color(surface: Surface, color: Color) -> Surface:
return surf
-def run() -> None:
- """Run test of module."""
+def auto_crop_clear(
+ surface: Surface,
+ clear: Color | None = None,
+) -> Surface:
+ """Remove unneccicary pixels from image."""
+ if clear is None:
+ clear = Color(0, 0, 0, 0)
+ surface = surface.convert_alpha()
+ w, h = surface.get_size()
+ surface.lock()
+
+ def find_end(
+ iterfunc: Callable[[int], Iterable[Color]],
+ rangeobj: Iterable[int],
+ ) -> int:
+ for x in rangeobj:
+ if not all(y == clear for y in iterfunc(x)):
+ return x
+ return x
+
+ def column(x: int) -> Generator[Color, None, None]:
+ return (surface.get_at((x, y)) for y in range(h))
+
+ def row(y: int) -> Generator[Color, None, None]:
+ return (surface.get_at((x, y)) for x in range(w))
+
+ leftc = find_end(column, range(w))
+ rightc = find_end(column, range(w - 1, -1, -1))
+ topc = find_end(row, range(h))
+ floorc = find_end(row, range(h - 1, -1, -1))
+ surface.unlock()
+ dim = Rect(leftc, topc, rightc - leftc, floorc - topc)
+ return surface.subsurface(dim)
if __name__ == "__main__":
print(f"{__title__}\nProgrammed by {__author__}.\n")
- run()
diff --git a/src/azul/game.py b/src/azul/game.py
index f26a9dc..58e6a00 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -32,7 +32,7 @@
import time
import traceback
from collections import Counter
-from functools import lru_cache, wraps
+from functools import lru_cache
from pathlib import Path
from typing import TYPE_CHECKING, Any, Final, TypeVar
@@ -60,6 +60,7 @@
from azul import element_list, objects, sprite
from azul.async_clock import Clock
from azul.client import GameClient, read_advertisements
+from azul.crop import auto_crop_clear
from azul.network_shared import DEFAULT_PORT
from azul.server import GameServer
from azul.sound import SoundData, play_sound as base_play_sound
@@ -83,6 +84,7 @@
)
from numpy.typing import NDArray
+ from pygame.sprite import LayeredDirty
from typing_extensions import TypeVarTuple
P = TypeVarTuple("P")
@@ -169,7 +171,12 @@
}
-@lru_cache
+def vec2_to_location(vec: Vector2) -> tuple[int, int]:
+ """Return rounded location tuple from Vector2."""
+ x, y = map(int, vec.rounded())
+ return x, y
+
+
def make_square_surf(
color: (
pygame.color.Color
@@ -229,41 +236,6 @@ def outline_rectangle(
return surface
-def auto_crop_clear(
- surface: pygame.surface.Surface,
- clear: pygame.color.Color | None = None,
-) -> pygame.surface.Surface:
- """Remove unneccicary pixels from image."""
- if clear is None:
- clear = pygame.color.Color(0, 0, 0, 0)
- surface = surface.convert_alpha()
- w, h = surface.get_size()
- surface.lock()
-
- def find_end(
- iterfunc: Callable[[int], Iterable[pygame.color.Color]],
- rangeobj: Iterable[int],
- ) -> int:
- for x in rangeobj:
- if not all(y == clear for y in iterfunc(x)):
- return x
- return x
-
- def column(x: int) -> Generator[pygame.color.Color, None, None]:
- return (surface.get_at((x, y)) for y in range(h))
-
- def row(y: int) -> Generator[pygame.color.Color, None, None]:
- return (surface.get_at((x, y)) for x in range(w))
-
- leftc = find_end(column, range(w))
- rightc = find_end(column, range(w - 1, -1, -1))
- topc = find_end(row, range(h))
- floorc = find_end(row, range(h - 1, -1, -1))
- surface.unlock()
- dim = pygame.rect.Rect(leftc, topc, rightc - leftc, floorc - topc)
- return surface.subsurface(dim)
-
-
@lru_cache
def get_tile_color(
tile_color: int,
@@ -338,9 +310,6 @@ def add_symbol_to_tile_surf(
surf.blit(symbolsurf, (int(x), int(y)))
-# surf.blit(symbolsurf, (0, 0))
-
-
def get_tile_image(
tile_color: int,
tilesize: int,
@@ -364,6 +333,7 @@ def get_tile_image(
add_symbol_to_tile_surf(surf, tile_color, tilesize, greyshift)
return surf
+ assert isinstance(color[0], int)
surf = make_square_surf(color, tilesize)
# Add tile symbol
add_symbol_to_tile_surf(surf, tile_color, tilesize, greyshift)
@@ -432,7 +402,7 @@ def clear_image(
size += offset
self.image = get_tile_container_image(
- round(size),
+ vec2_to_location(size),
self.background,
)
@@ -573,7 +543,7 @@ async def handle_cursor_reached_destination(
def move_to_front(self) -> None:
"""Move this sprite to front."""
- group: sprite.LayeredDirty = self.groups()[-1]
+ group: LayeredDirty = self.groups()[-1]
group.move_to_front(self)
async def handle_cursor_set_destination(
@@ -733,9 +703,12 @@ def __init__(self, board_id: int) -> None:
self.update_location_on_resize = True
+ # Clear image so rect is set
+ self.clear_image((5, 5))
+
def __repr__(self) -> str:
"""Return representation of self."""
- return f"{self.__class__.__name__}({self.variant_play})"
+ return f"{self.__class__.__name__}({self.board_id})"
def bind_handlers(self) -> None:
"""Register event handlers."""
@@ -766,7 +739,7 @@ async def handle_game_board_data(
class Row(TileRenderer):
"""Represents one of the five rows each player has."""
- __slots__ = ("color", "size", "tiles")
+ __slots__ = ("color", "count", "size")
greyshift = GREYSHIFT
def __init__(
@@ -795,16 +768,17 @@ def update_image(self) -> None:
"""Update self.image."""
self.clear_image((self.size, 1))
- for x in range(self.count):
- self.blit_tile(self.color, (x, 0))
for x in range(self.count, self.size):
- self.blit_tile(Tile.blank, (x, 0))
+ self.blit_tile(Tile.blank, (self.size - x, 0))
+ for x in range(self.count):
+ self.blit_tile(self.color, (self.size - x, 0))
+ self.dirty = 1
def get_tile_point(
self,
screen_location: tuple[int, int] | Vector2,
) -> int | None:
- """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections."""
+ """Return the x choordinate of which tile intersects given a point. Returns None if no intersections."""
pos = super().get_tile_point(screen_location)
if pos is None:
return None
@@ -828,6 +802,79 @@ def set_background(self, color: tuple[int, int, int] | None) -> None:
self.update_image()
+class PatternRows(TileRenderer):
+ """Represents one of the five rows each player has."""
+
+ __slots__ = (
+ "rows",
+ "rows_id",
+ )
+ greyshift = GREYSHIFT
+
+ def __init__(
+ self,
+ rows_id: int,
+ ) -> None:
+ """Initialize row."""
+ super().__init__(f"Pattern_Rows_{rows_id}", background=None)
+
+ self.add_component(sprite.DragClickEventComponent())
+
+ self.rows_id = rows_id
+ self.rows: dict[int, tuple[int, int]] = {
+ i: (Tile.blank, 0) for i in range(5)
+ }
+
+ self.update_image()
+ self.visible = True
+
+ def __repr__(self) -> str:
+ """Return representation of self."""
+ return f"{self.__class__.__name__}({self.rows_id})"
+
+ def update_image(self) -> None:
+ """Update self.image."""
+ self.clear_image((5, 5))
+
+ for y in range(5):
+ tile_color, count = self.rows[y]
+ for x in range(count, (y + 1)):
+ self.blit_tile(Tile.blank, (4 - x, y))
+ for x in range(count):
+ self.blit_tile(tile_color, (4 - x, y))
+ self.dirty = 1
+
+ def set_row_data(
+ self,
+ row_id: int,
+ tile_color: Tile,
+ tile_count: int,
+ ) -> None:
+ """Set row data and update image."""
+ assert row_id in self.rows
+ self.rows[row_id] = (tile_color, tile_count)
+ self.update_image()
+
+ def get_tile_point(
+ self,
+ screen_location: tuple[int, int] | Vector2,
+ ) -> int | None:
+ """Return the x choordinate of which tile intersects given a point. Returns None if no intersections."""
+ point = super().get_tile_point(screen_location)
+ if point is None:
+ return None
+ x, y = point
+ # If point is not valid for that row, say invalid
+ if (4 - x) > y:
+ return None
+ return point
+
+ def set_background(self, color: tuple[int, int, int] | None) -> None:
+ """Set the background color for this row."""
+ self.background = color
+ self.update_image()
+
+
class FloorLine(Row):
"""Represents a player's floor line."""
@@ -885,9 +932,11 @@ def __init__(self, factory_id: int) -> None:
self.update_location_on_resize = True
+ self.add_component(sprite.DragClickEventComponent())
+
def __repr__(self) -> str:
"""Return representation of self."""
- return f"{self.__class__.__name__}({self.number})"
+ return f"{self.__class__.__name__}({self.factory_id})"
def bind_handlers(self) -> None:
"""Register event handlers."""
@@ -935,6 +984,7 @@ def update_image(self) -> None:
for index, tile_color in enumerate(self.tiles.elements()):
y, x = divmod(index, 2)
self.blit_tile(tile_color, (x, y), (8, 8))
+ self.dirty = 1
def get_tile_point(
self,
@@ -1116,7 +1166,7 @@ class MenuState(GameState):
fontsize = BUTTONFONTSIZE
def __init__(self, name: str) -> None:
- """Initialize GameState and set up self.bh."""
+ """Initialize GameState and set up 30."""
super().__init__(name)
def add_button(
@@ -1127,7 +1177,7 @@ def add_button(
location: tuple[int, int] | None = None,
size: int = fontsize,
minlen: int = button_minimum,
- ) -> int:
+ ) -> None:
"""Add a new objects.Button object to group."""
button = KwargButton(
name,
@@ -1148,7 +1198,7 @@ def add_text(
color: tuple[int, int, int] = BUTTON_TEXT_COLOR,
size: int = fontsize,
outline: tuple[int, int, int] = BUTTON_TEXT_OUTLINE,
- ) -> int:
+ ) -> None:
"""Add a new objects.Text object to self.game with arguments. Return text id."""
text = KwargOutlineText(
name,
@@ -1196,60 +1246,6 @@ def to_state_by_attributes() -> None:
return to_state_by_attributes
- def with_update(
- self,
- update_function: Callable[[], None],
- ) -> Callable[[Callable[[], None]], Callable[[], None]]:
- """Return a wrapper for a function that will call update_function after function."""
-
- def update_wrapper(function: Callable[[], None]) -> Callable[[], None]:
- """Wrap anything that might require a screen update."""
-
- @wraps(function)
- def function_with_update() -> None:
- """Call main function, then update function."""
- function()
- update_function()
-
- return function_with_update
-
- return update_wrapper
-
- def update_text(
- self,
- text_name: str,
- value_function: Callable[[], str],
- ) -> Callable[[], None]:
- """Update text object with text_name's display value."""
-
- def updater() -> None:
- """Update text object {text_name}'s value with {value_function}."""
- assert self.game is not None
- text = self.game.get_object_by_name(f"objects.Text{text_name}")
- assert isinstance(text, objects.Text)
- text.update_value(value_function())
-
- return updater
-
- def toggle_button_state(
- self,
- textname: str,
- boolattr: str,
- textfunc: Callable[[bool], str],
- ) -> Callable[[], None]:
- """Return function that will toggle the value of text object , toggling attribute , and setting text value with textfunc."""
-
- def valfunc() -> str:
- """Return the new value for the text object. Gets called AFTER value is toggled."""
- return textfunc(getattr(self, boolattr))
-
- @self.with_update(self.update_text(textname, valfunc))
- def toggle_value() -> None:
- """Toggle the value of boolattr."""
- self.set_var(boolattr, not getattr(self, boolattr))
-
- return toggle_value
-
class InitializeState(AsyncState["AzulClient"]):
"""Initialize state."""
@@ -1442,13 +1438,13 @@ def host_text(x: object) -> str:
self.add_text(
"Host",
host_text(self.host_mode),
- (cx, cy - self.bh * 3),
+ (cx, cy - 30 * 3),
)
self.add_button(
"ToggleHost",
"Toggle",
self.toggle_button_state("Host", "host_mode", host_text),
- (cx, cy - self.bh * 2),
+ (cx, cy - 30 * 2),
size=int(self.fontsize / 1.5),
)
@@ -1462,7 +1458,7 @@ def varient_text(x: object) -> str:
self.add_text(
"Variant",
varient_text(self.variant_play),
- (cx, cy - self.bh),
+ (cx, cy - 30),
)
self.add_button(
"ToggleVarient",
@@ -1475,9 +1471,9 @@ def varient_text(x: object) -> str:
self.add_text(
"Players",
f"Players: {self.player_count}",
- (cx, cy + self.bh),
+ (cx, cy + 30),
)
- add_numbers(2, 4, 70, cx, int(cy + self.bh * 2))
+ add_numbers(2, 4, 70, cx, int(cy + 30 * 2))
var_to_state = self.var_dependant_to_state(
FactoryOffer=("host_mode", True),
@@ -1487,142 +1483,8 @@ def varient_text(x: object) -> str:
"StartGame",
"Start Game",
var_to_state,
- (cx, cy + self.bh * 3),
- )
-
- def exit_actions(self) -> None:
- """Start game."""
- assert self.game is not None
- self.game.start_game(
- self.player_count,
- self.variant_play,
- self.host_mode,
- )
- self.game.bag.full_reset()
-
-
-class PhaseFactoryOffer(GameState):
- """Game state when it's the Factory Offer Stage."""
-
- __slots__ = ()
-
- def __init__(self) -> None:
- """Initialize factory offer phase."""
- super().__init__("FactoryOffer")
-
-
-class PhaseFactoryOfferNetworked(PhaseFactoryOffer):
- """Factory offer phase but networked."""
-
- __slots__ = ()
-
- def __init__(self) -> None:
- """Initialize factory offer networked."""
- GameState.__init__(self, "FactoryOfferNetworked")
-
- def check_state(self) -> str:
- """Go to networked wall tiling."""
- return "WallTilingNetworked"
-
-
-class PhaseWallTiling(GameState):
- """Wall tiling game phase."""
-
- # __slots__ = ()
- def __init__(self) -> None:
- """Initialize will tiling phase."""
- super().__init__("WallTiling")
-
- def entry_actions(self) -> None:
- """Start wall tiling."""
- assert self.game is not None
- self.next_starter: int = 0
- self.not_processed = []
-
- self.game.player_turn_over()
-
- # For each player,
- for player_id in range(self.game.players):
- # Activate wall tiling mode.
- player = self.game.get_player(player_id)
- player.wall_tiling()
- # Add that player's player_id to the list of not-processed players.
- self.not_processed.append(player.player_id)
-
- # Start processing players.
- self.game.next_turn()
-
- def do_actions(self) -> None:
- """Do game actions."""
- assert self.game is not None
- if self.not_processed:
- if self.game.player_turn in self.not_processed:
- player = self.game.get_player(self.game.player_turn)
- if player.done_wall_tiling():
- # Once player is done wall tiling, score their moves.
- # Also gets if they had the number one tile.
- number_one = player.score_phase()
-
- if number_one:
- # If player had the number one tile, remember that.
- self.next_starter = self.game.player_turn
- # Then, add the number one tile back to the table center.
- table = self.game.get_object_by_name("TableCenter")
- assert isinstance(table, TableCenter)
- table.add_number_one_tile()
- # After calculating their score, delete player from un-processed list
- self.not_processed.remove(self.game.player_turn)
- # and continue to the next un-processed player.
- self.game.next_turn()
- else:
- self.game.next_turn()
-
- def check_state(self) -> str | None:
- """Go to next state if ready."""
- assert self.game is not None
- cursor = self.game.get_object_by_name("Cursor")
- assert isinstance(cursor, Cursor)
- if not self.not_processed and not cursor.is_holding():
- return "PrepareNext"
- return None
-
- def exit_actions(self) -> None:
- """Update who's turn it is."""
- assert self.game is not None
- # Set up the player that had the number one tile to be the starting player next round.
- self.game.player_turn_over()
- # Goal: make (self.player_turn + 1) % self.players = self.next_starter
- nturn = self.next_starter - 1
- if nturn < 0:
- nturn += self.game.players
- self.game.player_turn = nturn
-
-
-class PhasePrepareNext(GameState):
- """Prepare next phase of game."""
-
- __slots__ = ("new_round",)
-
- def __init__(self) -> None:
- """Initialize prepare next state."""
- super().__init__("PrepareNext")
- self.new_round = False
-
- def entry_actions(self) -> None:
- """Find out if game continues."""
- assert self.game is not None
- players = (
- self.game.get_player(player_id)
- for player_id in range(self.game.players)
+ (cx, cy + 30 * 3),
)
- complete = (player.has_horzontal_line() for player in players)
- self.new_round = not any(complete)
-
- def check_state(self) -> str:
- """Go to factory offer if new round else end screen."""
- if self.new_round:
- return "FactoryOffer"
- return "End"
class EndScreen(MenuState):
@@ -1838,27 +1700,6 @@ async def handle_return_to_title(self, _: Event[None]) -> None:
await self.machine.set_state("title")
-# async def check_conditions(self) -> str | None:
-# return None
-
-
-## async def entry_actions(self) -> None:
-## """Set up buttons."""
-## assert self.machine is not None
-## self.id = self.machine.new_group("initialize")
-##
-## self.group_add(Cursor())
-## await self.manager.raise_event(Event("cursor_drag", [3, 5]))
-## self.manager.register_handler("PygameMouseMotion", self.mouse_moved)
-##
-## ## board = Board()
-## #### board.place_tile((2, 2), Tile.red)
-## ## board.location = Vector2.from_iter(SCREEN_SIZE) // 2
-## ## self.group_add(board)
-##
-## center.add_tiles((0, 1, 2, 3, 5))
-
-
class PlayState(GameState):
"""Game Play State."""
@@ -1901,11 +1742,11 @@ async def entry_actions(self) -> None:
self.group_add(center)
# self.group_add(())
- ## gameboard = GameBoard(
- ## 45,
- ## )
- ## gameboard.location = [x // 2 for x in SCREEN_SIZE]
- ## self.group_add(gameboard)
+ ##gameboard = GameBoard(
+ ## 45,
+ ##)
+ ##gameboard.location = [x // 2 for x in SCREEN_SIZE]
+ ##self.group_add(gameboard)
async def handle_game_initial_config(
self,
@@ -1919,29 +1760,41 @@ async def handle_game_initial_config(
center = Vector2.from_iter(SCREEN_SIZE) // 2
# Add factories
- for index, degrees in enumerate(range(0, 360, 360 // factory_count)):
+ each = 360 / factory_count
+ degrees: float = -90
+ for index in range(factory_count):
factory = Factory(index)
- factory.location = (
+ factory.location = vec2_to_location(
Vector2.from_degrees(
- degrees - 90,
+ degrees,
145,
)
- + center
+ + center,
)
self.group_add(factory)
+ degrees += each
+
# Add players
- for index, degrees in enumerate(range(0, 360, 360 // player_count)):
+ each = 360 / player_count
+ degrees = -(90 / player_count)
+ for index in range(player_count):
board = Board(index)
- board.location = (
+ board.rect.midleft = vec2_to_location(
Vector2.from_degrees(
- degrees - 45,
+ degrees,
300,
)
- + center
+ + center,
)
self.group_add(board)
+ pattern_rows = PatternRows(index)
+ pattern_rows.rect.bottomright = board.rect.bottomleft
+ self.group_add(pattern_rows)
+
+ degrees += each
+
async def check_conditions(self) -> str | None:
"""Return to title if client component doesn't exist."""
if not self.machine.manager.component_exists("network"):
@@ -1980,8 +1833,6 @@ async def handle_client_disconnected(self, event: Event[str]) -> None:
self.exit_data = (1, f"Client Disconnected$${error}", False)
- # await self.do_actions()
-
async def do_actions(self) -> None:
"""Perform actions for this State."""
# print(f"{self.__class__.__name__} do_actions tick")
@@ -2208,7 +2059,7 @@ def cli_run() -> None:
run()
except ExceptionGroup as exc:
print(exc)
- exception = traceback.format_exception(exc)
+ exception = "".join(traceback.format_exception(exc))
## raise
## except BaseException as ex:
## screenshot_last_frame()
@@ -2217,7 +2068,7 @@ def cli_run() -> None:
finally:
pygame.quit()
if exception is not None:
- print("".join(exception), file=sys.stderr)
+ print(exception, file=sys.stderr)
if __name__ == "__main__":
diff --git a/src/azul/mr_floppy_test.py b/src/azul/mr_floppy_test.py
index 1b8a772..5275dda 100644
--- a/src/azul/mr_floppy_test.py
+++ b/src/azul/mr_floppy_test.py
@@ -257,7 +257,7 @@ def controller(
async def drag(self, event: Event[sprite.DragEvent]) -> None:
"""Move by relative from drag."""
- if event.data.button != 1:
+ if not event.data.buttons[1]:
return
self.location += event.data.rel
self.dirty = 1
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index e55ea4e..ba625d4 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -597,7 +597,7 @@ class DragEvent(NamedTuple):
pos: tuple[int, int]
rel: tuple[int, int]
- button: int
+ buttons: dict[int, bool]
class DragClickEventComponent(Component):
@@ -672,20 +672,17 @@ async def motion(
if not self.manager_exists:
return
async with trio.open_nursery() as nursery:
- for button, pressed in self.pressed.items():
- if not pressed:
- continue
- nursery.start_soon(
- self.raise_event,
- Event(
- "drag",
- DragEvent(
- event.data["pos"],
- event.data["rel"],
- button,
- ),
+ nursery.start_soon(
+ self.raise_event,
+ Event(
+ "drag",
+ DragEvent(
+ event.data["pos"],
+ event.data["rel"],
+ self.pressed,
),
- )
+ ),
+ )
class GroupProcessor(AsyncStateMachine):
From db3f8c3767933e1bfb153f787bab6291c68baf34 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 29 Nov 2024 19:20:21 -0600
Subject: [PATCH 12/67] Vectorize detection
---
src/azul/game.py | 31 +++++++------------------------
1 file changed, 7 insertions(+), 24 deletions(-)
diff --git a/src/azul/game.py b/src/azul/game.py
index 58e6a00..968adc4 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -413,26 +413,19 @@ def blit_tile(
offset: tuple[int, int] | None = None,
) -> None:
"""Blit the surface of a given tile object onto self.image at given tile location. It is assumed that all tile locations are xy tuples."""
- x, y = tile_location
- if offset is None:
- ox, oy = 0, 0
- else:
- ox, oy = offset
+ tile_full = self.tile_size + self.tile_separation
- ox += self.tile_separation
- oy += self.tile_separation
+ position = Vector2.from_iter(tile_location) * tile_full
+ if offset is not None:
+ position += offset
+ position += (self.tile_separation, self.tile_separation)
surf = get_tile_image(tile_color, self.tile_size, self.greyshift)
assert self.image is not None
- tile_full = self.tile_size + self.tile_separation
-
self.image.blit(
surf,
- (
- round(x * tile_full + ox),
- round(y * tile_full + oy),
- ),
+ vec2_to_location(position),
)
def to_image_surface_location(
@@ -774,16 +767,6 @@ def update_image(self) -> None:
self.blit_tile(self.color, (self.size - x, 0))
self.dirty = 1
- def get_tile_point(
- self,
- screen_location: tuple[int, int] | Vector2,
- ) -> int | None:
- """Return the x choordinate of which tile intersects given a point. Returns None if no intersections."""
- pos = super().get_tile_point(screen_location)
- if pos is None:
- return None
- return pos[0]
-
def get_placed(self) -> int:
"""Return the number of tiles in self that are not fake tiles, like grey ones."""
return self.count
@@ -858,7 +841,7 @@ def set_row_data(
def get_tile_point(
self,
screen_location: tuple[int, int] | Vector2,
- ) -> int | None:
+ ) -> tuple[int, int] | None:
"""Return the x choordinate of which tile intersects given a point. Returns None if no intersections."""
point = super().get_tile_point(screen_location)
if point is None:
From 60ea0f6cf37f29500b12bca4a1eee3814a147789 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 29 Nov 2024 21:00:35 -0600
Subject: [PATCH 13/67] Fix a ton of type and lint errors
---
src/azul/game.py | 166 ++++++----------------------------------------
src/azul/state.py | 11 ++-
2 files changed, 27 insertions(+), 150 deletions(-)
diff --git a/src/azul/game.py b/src/azul/game.py
index 968adc4..77fa66c 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -56,6 +56,7 @@
WINDOWRESIZED,
)
from pygame.rect import Rect
+from pygame.sprite import LayeredDirty
from azul import element_list, objects, sprite
from azul.async_clock import Clock
@@ -84,7 +85,6 @@
)
from numpy.typing import NDArray
- from pygame.sprite import LayeredDirty
from typing_extensions import TypeVarTuple
P = TypeVarTuple("P")
@@ -438,9 +438,11 @@ def to_image_surface_location(
def get_tile_point(
self,
screen_location: tuple[int, int] | Vector2,
- ) -> tuple[int, int] | None:
+ ) -> Vector2 | None:
"""Return the xy choordinates of which tile intersects given a point or None."""
# Can't get tile if screen location doesn't intersect our hitbox!
+ if isinstance(screen_location, Vector2):
+ screen_location = vec2_to_location(screen_location)
if not self.is_selected(screen_location):
return None
@@ -536,7 +538,8 @@ async def handle_cursor_reached_destination(
def move_to_front(self) -> None:
"""Move this sprite to front."""
- group: LayeredDirty = self.groups()[-1]
+ group = self.groups()[-1]
+ assert isinstance(group, LayeredDirty)
group.move_to_front(self)
async def handle_cursor_set_destination(
@@ -656,7 +659,7 @@ def fake_tile_exists(self, xy: tuple[int, int]) -> bool:
"""Return if tile at given position is a fake tile."""
return self.get_tile(xy) < 0
- def place_tile(self, xy: tuple[int, int], tile_color: int) -> bool:
+ def place_tile(self, xy: tuple[int, int], tile_color: int) -> None:
"""Place tile at given position."""
x, y = xy
self.data[y, x] = tile_color
@@ -841,14 +844,13 @@ def set_row_data(
def get_tile_point(
self,
screen_location: tuple[int, int] | Vector2,
- ) -> tuple[int, int] | None:
+ ) -> Vector2 | None:
"""Return the x choordinate of which tile intersects given a point. Returns None if no intersections."""
point = super().get_tile_point(screen_location)
if point is None:
return None
- x, y = point
# If point is not valid for that row, say invalid
- if (4 - x) > y:
+ if (4 - point.x) > point.y:
return None
return point
@@ -869,34 +871,31 @@ def __init__(self, floor_line_id: int) -> None:
# self.font = Font(FONT, round(self.tile_size*1.2), color=BLACK, cx=False, cy=False)
self.text = objects.Text(
- round(self.tile_size * 1.2),
- BLACK,
- cx=False,
- cy=False,
+ "text object",
+ pygame.font.Font(FONT, round(self.tile_size * 1.2)),
)
+ self.text.color = BLACK
self.numbers = [-255 for _ in range(self.size)]
def __repr__(self) -> str:
"""Return representation of self."""
- return f"{self.__class__.__name__}({self.player!r})"
+ return f"{self.__class__.__name__}(<>)"
def render(self, surface: pygame.surface.Surface) -> None:
"""Update self.image."""
- super().render(surface)
-
sx, sy = self.location
- assert self.width_height is not None, "Should be impossible."
- w, h = self.width_height
+ w, h = self.rect.size
+ tile_full = self.tile_separation + self.tile_size
for x in range(self.size):
xy = round(
- x * self.tile_full + self.tile_separation + sx - w / 2,
+ x * tile_full + self.tile_separation + sx - w / 2,
), round(
self.tile_separation + sy - h / 2,
)
- self.text.update_value(str(self.numbers[x]))
+ self.text.text = str(self.numbers[x])
self.text.location = Vector2(*xy)
- self.text.render(surface)
+ # self.text.render(surface)
class Factory(TileRenderer):
@@ -972,7 +971,7 @@ def update_image(self) -> None:
def get_tile_point(
self,
screen_location: tuple[int, int] | Vector2,
- ) -> tuple[int, int] | None:
+ ) -> Vector2 | None:
"""Get tile point accounting for offset."""
return super().get_tile_point(
Vector2.from_iter(screen_location) - (8, 8),
@@ -1344,132 +1343,6 @@ def check_state(self) -> str:
return "title"
-class SettingsState(MenuState):
- """Game state when user is defining game type, players, etc."""
-
- def __init__(self) -> None:
- """Initialize settings."""
- super().__init__("settings")
-
- self.player_count = 0 # 2
- self.host_mode = True
- self.variant_play = False
-
- async def entry_actions(self) -> None:
- """Add cursor object and tons of button and text objects to the game."""
- await super().entry_actions()
-
- def add_numbers(
- start: int,
- end: int,
- width_each: int,
- cx: int,
- cy: int,
- ) -> None:
- """Add numbers."""
- count = end - start + 1
- evencount = count % 2 == 0
- mid = count // 2
-
- def add_number(
- number: int,
- display: str | int,
- ) -> None:
- """Add number."""
- if evencount:
- if number < mid:
- x = number - start - 0.5
- else:
- x = number - mid + 0.5
- else:
- if number < mid:
- x = number - start + 1
- elif number == mid:
- x = 0
- else:
- x = number - mid
-
- @self.with_update(
- self.update_text(
- "Players",
- lambda: f"Players: {self.player_count}",
- ),
- )
- def set_player_count() -> None:
- """Set variable player_count to {display} while updating text."""
- return self.set_var("player_count", display)
-
- self.add_button(
- f"SetCount{number}",
- str(display),
- set_player_count,
- (int(cx + (width_each * x)), int(cy)),
- size=int(self.fontsize / 1.5),
- minlen=3,
- )
-
- for i in range(count):
- add_number(i, start + i)
-
- sw, sh = SCREEN_SIZE
- cx = sw // 2
- cy = sh // 2
-
- def host_text(x: object) -> str:
- return f"Host Mode: {x}"
-
- self.add_text(
- "Host",
- host_text(self.host_mode),
- (cx, cy - 30 * 3),
- )
- self.add_button(
- "ToggleHost",
- "Toggle",
- self.toggle_button_state("Host", "host_mode", host_text),
- (cx, cy - 30 * 2),
- size=int(self.fontsize / 1.5),
- )
-
- ## # TEMPORARY: Hide everything to do with "Host Mode", networked games aren't done yet.
- ## assert self.game is not None
- ## self.game.set_attr_all("visible", False)
-
- def varient_text(x: object) -> str:
- return f"Variant Play: {x}"
-
- self.add_text(
- "Variant",
- varient_text(self.variant_play),
- (cx, cy - 30),
- )
- self.add_button(
- "ToggleVarient",
- "Toggle",
- self.toggle_button_state("Variant", "variant_play", varient_text),
- (cx, cy),
- size=int(self.fontsize / 1.5),
- )
-
- self.add_text(
- "Players",
- f"Players: {self.player_count}",
- (cx, cy + 30),
- )
- add_numbers(2, 4, 70, cx, int(cy + 30 * 2))
-
- var_to_state = self.var_dependant_to_state(
- FactoryOffer=("host_mode", True),
- FactoryOfferNetworked=("host_mode", False),
- )
- self.add_button(
- "StartGame",
- "Start Game",
- var_to_state,
- (cx, cy + 30 * 3),
- )
-
-
class EndScreen(MenuState):
"""End screen state."""
@@ -1888,7 +1761,6 @@ def __init__(self, manager: ExternalRaiseManager) -> None:
InitializeState(),
TitleState(),
CreditsState(),
- SettingsState(),
PlayHostingState(),
PlayInternalHostingState(),
PlayJoiningState(),
diff --git a/src/azul/state.py b/src/azul/state.py
index 755bc9b..0041124 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -104,7 +104,9 @@ def generate_bag_contents() -> Counter[int]:
def bag_draw_tile(bag: Counter[int]) -> int:
"""Return drawn tile from bag. Mutates bag."""
- tile = random.choice(tuple(bag.elements()))
+ # S311 Standard pseudo-random generators are not suitable for
+ # cryptographic purposes
+ tile = random.choice(tuple(bag.elements())) # noqa: S311
bag[tile] -= 1
return tile
@@ -651,6 +653,7 @@ class State(NamedTuple):
@classmethod
def blank(cls) -> Self:
+ """Return new blank state."""
return cls(
varient_play=False,
current_phase=Phase.end,
@@ -1230,7 +1233,7 @@ def manual_wall_tiling_action(
result = new_state.get_manual_wall_tiling_locations_for_player(
player_id,
)
- if isinstance(result, tuple) or result is None:
+ if not isinstance(result, self.__class__):
return new_state._manual_wall_tiling_maybe_next_turn()
return result._manual_wall_tiling_maybe_next_turn()
@@ -1249,7 +1252,9 @@ def run() -> None:
## last_turn = state.current_turn
actions = tuple(state.yield_actions())
print(f"{len(actions) = }")
- action = random.choice(actions)
+ # S311 Standard pseudo-random generators are not suitable
+ # for cryptographic purposes
+ action = random.choice(actions) # noqa: S311
## pprint(action)
state = state.preform_action(action)
From 988271c7cb4b9b585bd1fc1f0f6af1f8d889bfb7 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 30 Nov 2024 00:08:33 -0700
Subject: [PATCH 14/67] Implement more parts of grabbing tile logic
---
src/azul/client.py | 84 +++++++++++++
src/azul/game.py | 141 +++++++++++++++++----
src/azul/network_shared.py | 6 +
src/azul/server.py | 250 ++++++++++++++++++++++++++++++++++---
src/azul/state.py | 2 +-
src/azul/vector.py | 6 +
6 files changed, 447 insertions(+), 42 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index 64ac3f4..2d9be7c 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -47,6 +47,9 @@
if TYPE_CHECKING:
from mypy_extensions import u8
+ from azul.state import Tile
+ from azul.vector import Vector2
+
async def read_advertisements(
timeout: int = 3, # noqa: ASYNC109
@@ -165,6 +168,9 @@ def __init__(self, name: str) -> None:
self.register_network_write_events(
{
"encryption_response->server": sbe.encryption_response,
+ "factory_clicked->server[write]": sbe.factory_clicked,
+ "cursor_location->server[write]": sbe.cursor_location,
+ "pattern_row_clicked->server[write]": sbe.pattern_row_clicked,
},
)
cbe = ClientBoundEvents
@@ -177,6 +183,9 @@ def __init__(self, name: str) -> None:
cbe.game_over: "server->game_over",
cbe.board_data: "server->board_data",
cbe.factory_data: "server->factory_data",
+ cbe.cursor_data: "server->cursor_data",
+ cbe.table_data: "server->table_data",
+ cbe.cursor_movement_mode: "server->cursor_movement_mode",
},
)
@@ -195,8 +204,14 @@ def bind_handlers(self) -> None:
"server->game_over": self.read_game_over,
"server->board_data": self.read_board_data,
"server->factory_data": self.read_factory_data,
+ "server->cursor_data": self.read_cursor_data,
+ "server->table_data": self.read_table_data,
+ "server->cursor_movement_mode": self.read_cursor_movement_mode,
"client_connect": self.handle_client_connect,
"network_stop": self.handle_network_stop,
+ "game_factory_clicked": self.write_game_factory_clicked,
+ "game_cursor_location_transmit": self.write_game_cursor_location_transmit,
+ "game_pattern_row_clicked": self.write_game_pattern_row_clicked,
},
)
@@ -371,6 +386,75 @@ async def read_factory_data(self, event: Event[bytearray]) -> None:
await self.raise_event(Event("game_factory_data", (factory_id, tiles)))
+ async def read_cursor_data(self, event: Event[bytearray]) -> None:
+ """Read cursor_data event from server, reraise as `game_cursor_data`."""
+ buffer = Buffer(event.data)
+
+ tiles = decode_numeric_uint8_counter(buffer)
+
+ await self.raise_event(Event("game_cursor_data", tiles))
+
+ async def read_table_data(self, event: Event[bytearray]) -> None:
+ """Read table_data event from server, reraise as `game_table_data`."""
+ buffer = Buffer(event.data)
+
+ tiles = decode_numeric_uint8_counter(buffer)
+
+ await self.raise_event(Event("game_table_data", tiles))
+
+ async def read_cursor_movement_mode(self, event: Event[bytearray]) -> None:
+ """Read cursor_movement_mode event from server, reraise as `game_cursor_set_movement_mode`."""
+ buffer = Buffer(event.data)
+
+ client_mode = buffer.read_value(StructFormat.BOOL)
+
+ await self.raise_event(
+ Event("game_cursor_set_movement_mode", client_mode),
+ )
+
+ async def write_game_factory_clicked(
+ self,
+ event: Event[tuple[int, Tile]],
+ ) -> None:
+ """Write factory_clicked event to server."""
+ factory_id, tile = event.data
+ buffer = Buffer()
+
+ buffer.write_value(StructFormat.UBYTE, factory_id)
+ buffer.write_value(StructFormat.UBYTE, tile)
+
+ await self.raise_event(Event("factory_clicked->server[write]", buffer))
+
+ async def write_game_cursor_location_transmit(
+ self,
+ event: Event[Vector2],
+ ) -> None:
+ """Write cursor_location_transmit event to server."""
+ scaled_location = event.data
+
+ x, y = map(int, (scaled_location * 4096).rounded()) # 2 ** 12
+
+ position = (x & 0xFFF) << 3 | (y & 0xFFF)
+ buffer = position.to_bytes(3, "big")
+
+ await self.raise_event(Event("cursor_location->server[write]", buffer))
+
+ async def write_game_pattern_row_clicked(
+ self,
+ event: Event[tuple[int, Vector2]],
+ ) -> None:
+ """Write factory_clicked event to server."""
+ row_id, location = event.data
+ buffer = Buffer()
+
+ buffer.write_value(StructFormat.UBYTE, row_id)
+ buffer.write_value(StructFormat.UBYTE, int(location.x))
+ buffer.write_value(StructFormat.UBYTE, int(location.y))
+
+ await self.raise_event(
+ Event("pattern_row_clicked->server[write]", buffer),
+ )
+
async def handle_network_stop(self, event: Event[None]) -> None:
"""Send EOF if connected and close socket."""
if self.not_connected:
diff --git a/src/azul/game.py b/src/azul/game.py
index 77fa66c..b784c8c 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -461,7 +461,7 @@ def get_tile_point(
if value > self.tile_size:
return None
# Otherwise, not in separation region, so we should be good
- return tile_position
+ return tile_position.floored()
class Cursor(TileRenderer):
@@ -511,22 +511,23 @@ def bind_handlers(self) -> None:
"""Register handlers."""
self.register_handlers(
{
- "cursor_drag": self.handle_cursor_drag,
+ "game_cursor_data": self.handle_cursor_drag,
"cursor_reached_destination": self.handle_cursor_reached_destination,
- "cursor_set_destination": self.handle_cursor_set_destination,
- "cursor_set_movement_mode": self.handle_cursor_set_movement_mode,
+ "game_cursor_set_destination": self.handle_cursor_set_destination,
+ "game_cursor_set_movement_mode": self.handle_cursor_set_movement_mode,
},
)
- async def handle_cursor_drag(self, event: Event[Iterable[int]]) -> None:
+ async def handle_cursor_drag(self, event: Event[Counter[int]]) -> None:
"""Drag one or more tiles."""
- await trio.lowlevel.checkpoint()
- for tile_color in event.data:
+ self.tiles.clear()
+ for tile_color in event.data.elements():
if tile_color == Tile.one:
self.tiles.insert(0, tile_color)
else:
self.tiles.append(tile_color)
self.update_image()
+ await trio.lowlevel.checkpoint()
async def handle_cursor_reached_destination(
self,
@@ -564,7 +565,20 @@ async def handle_pygame_mouse_motion(
"""Set location to event data."""
self.move_to_front()
self.location = event.data["pos"]
- await trio.lowlevel.checkpoint()
+
+ ##transmit_location = Vector2.from_iter(
+ ## x / y for x, y in zip(self.location, SCREEN_SIZE, strict=False)
+ ##)
+ ##
+ ### Transmit to server
+ ### Event level to so reaches client
+ ##await self.raise_event(
+ ## Event(
+ ## "game_cursor_location_transmit",
+ ## transmit_location,
+ ## 2,
+ ## )
+ ##)
async def handle_cursor_set_movement_mode(
self,
@@ -818,6 +832,10 @@ def __repr__(self) -> str:
"""Return representation of self."""
return f"{self.__class__.__name__}({self.rows_id})"
+ def bind_handlers(self) -> None:
+ """Register click event handler."""
+ self.register_handler("click", self.handle_click)
+
def update_image(self) -> None:
"""Update self.image."""
self.clear_image((5, 5))
@@ -859,6 +877,28 @@ def set_background(self, color: tuple[int, int, int] | None) -> None:
self.background = color
self.update_image()
+ async def handle_click(
+ self,
+ event: Event[sprite.PygameMouseButtonEventData],
+ ) -> None:
+ """Handle click event."""
+ point = self.get_tile_point(event.data["pos"])
+ if point is None:
+ await trio.lowlevel.checkpoint()
+ return
+
+ # Transmit to server
+ await self.raise_event(
+ Event(
+ "game_pattern_row_clicked",
+ (
+ self.rows_id,
+ point.floored(),
+ ),
+ 2,
+ ),
+ )
+
class FloorLine(Row):
"""Represents a player's floor line."""
@@ -925,26 +965,10 @@ def bind_handlers(self) -> None:
self.register_handlers(
{
"game_factory_data": self.handle_factory_data,
+ "click": self.handle_click,
},
)
- async def handle_factory_data(
- self,
- event: Event[tuple[int, Counter[int]]],
- ) -> None:
- """Handle `game_factory_data` event."""
- factory_id, tiles = event.data
-
- if factory_id != self.factory_id:
- await trio.lowlevel.checkpoint()
- return
-
- self.tiles = tiles
- self.update_image()
- self.visible = True
-
- await trio.lowlevel.checkpoint()
-
def update_image(self) -> None:
"""Update image."""
self.clear_image((2, 2), extra=(16, 16))
@@ -973,9 +997,62 @@ def get_tile_point(
screen_location: tuple[int, int] | Vector2,
) -> Vector2 | None:
"""Get tile point accounting for offset."""
- return super().get_tile_point(
+ point = super().get_tile_point(
Vector2.from_iter(screen_location) - (8, 8),
)
+ if point is None:
+ return None
+ if any(x >= 2 for x in point):
+ return None
+ return point
+
+ async def handle_factory_data(
+ self,
+ event: Event[tuple[int, Counter[int]]],
+ ) -> None:
+ """Handle `game_factory_data` event."""
+ factory_id, tiles = event.data
+
+ if factory_id != self.factory_id:
+ await trio.lowlevel.checkpoint()
+ return
+
+ self.tiles = tiles
+ self.update_image()
+ self.visible = True
+
+ await trio.lowlevel.checkpoint()
+
+ async def handle_click(
+ self,
+ event: Event[sprite.PygameMouseButtonEventData],
+ ) -> None:
+ """Handle click event."""
+ point = self.get_tile_point(event.data["pos"])
+ if point is None:
+ await trio.lowlevel.checkpoint()
+ return
+
+ index = int(point.y * 2 + point.x)
+ tile_color = tuple(self.tiles.elements())[index]
+
+ if tile_color < 0:
+ # Do not send non-real tiles
+ await trio.lowlevel.checkpoint()
+ return
+
+ # Transmit to server
+ # Needs level 2 to reach server client
+ await self.raise_event(
+ Event(
+ "game_factory_clicked",
+ (
+ self.factory_id,
+ Tile(tile_color),
+ ),
+ 2,
+ ),
+ )
class TableCenter(TileRenderer):
@@ -996,6 +1073,10 @@ def __repr__(self) -> str:
"""Return representation of self."""
return f"{self.__class__.__name__}()"
+ def bind_handlers(self) -> None:
+ """Register event handlers."""
+ self.register_handler("game_table_data", self.update_board_data)
+
def iter_tiles(self) -> Generator[int, None, None]:
"""Yield tile colors."""
count = 0
@@ -1044,6 +1125,12 @@ def pull_tiles(self, tile_color: int) -> list[int]:
tile_count = self.tiles.pop(tile_color)
return [tile_color] * tile_count
+ async def update_board_data(self, event: Event[Counter[int]]) -> None:
+ """Update table center board data."""
+ self.tiles = event.data
+ self.update_image()
+ await trio.lowlevel.checkpoint()
+
class HaltState(AsyncState["AzulClient"]):
"""Halt state to set state to None so running becomes False."""
@@ -1647,6 +1734,8 @@ async def handle_game_initial_config(
pattern_rows = PatternRows(index)
pattern_rows.rect.bottomright = board.rect.bottomleft
+ if index == self.current_turn:
+ pattern_rows.set_background(DARKGREEN)
self.group_add(pattern_rows)
degrees += each
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index 92adb3e..84168ef 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -118,9 +118,15 @@ class ClientBoundEvents(IntEnum):
game_over = auto()
board_data = auto()
factory_data = auto()
+ cursor_data = auto()
+ table_data = auto()
+ cursor_movement_mode = auto()
class ServerBoundEvents(IntEnum):
"""Server bound event IDs."""
encryption_response = 0
+ factory_clicked = auto()
+ cursor_location = auto()
+ pattern_row_clicked = auto()
diff --git a/src/azul/server.py b/src/azul/server.py
index 44adf47..6e1f2f7 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -29,6 +29,7 @@
import traceback
from collections import deque
+from enum import IntEnum, auto
from functools import partial
from typing import TYPE_CHECKING, NoReturn
@@ -55,7 +56,7 @@
encode_int8_array,
encode_numeric_uint8_counter,
)
-from azul.state import Phase, State
+from azul.state import Phase, State, Tile
if TYPE_CHECKING:
from collections import Counter
@@ -96,12 +97,18 @@ def __init__(self, client_id: int) -> None:
"server[write]->game_over": cbe.game_over,
"server[write]->board_data": cbe.board_data,
"server[write]->factory_data": cbe.factory_data,
+ "server[write]->cursor_data": cbe.cursor_data,
+ "server[write]->table_data": cbe.table_data,
+ "server[write]->cursor_movement_mode": cbe.cursor_movement_mode,
},
)
sbe = ServerBoundEvents
self.register_read_network_events(
{
sbe.encryption_response: f"client[{self.client_id}]->encryption_response",
+ sbe.factory_clicked: f"client[{self.client_id}]->factory_clicked",
+ sbe.cursor_location: f"client[{self.client_id}]->cursor_location",
+ sbe.pattern_row_clicked: f"client[{self.client_id}]->pattern_row_clicked",
},
)
@@ -111,12 +118,18 @@ def bind_handlers(self) -> None:
self.register_handlers(
{
f"client[{self.client_id}]->encryption_response": self.handle_encryption_response,
+ f"client[{self.client_id}]->factory_clicked": self.read_factory_clicked,
+ f"client[{self.client_id}]->cursor_location": self.read_cursor_location,
+ f"client[{self.client_id}]->pattern_row_clicked": self.read_pattern_row_clicked,
f"callback_ping->network[{self.client_id}]": self.handle_callback_ping,
- "initial_config->network": self.handle_initial_config,
- f"playing_as->network[{self.client_id}]": self.handle_playing_as,
- "game_over->network": self.handle_game_over,
- "board_data->network": self.handle_board_data,
- "factory_data->network": self.handle_factory_data,
+ "initial_config->network": self.write_factory_clicked,
+ f"playing_as->network[{self.client_id}]": self.write_playing_as,
+ "game_over->network": self.write_game_over,
+ "board_data->network": self.write_board_data,
+ "factory_data->network": self.write_factory_data,
+ "cursor_data->network": self.write_cursor_data,
+ "table_data->network": self.write_table_data,
+ f"cursor_movement_mode->network[{self.client_id}]": self.write_cursor_movement_mode,
},
)
@@ -131,6 +144,47 @@ async def start_encryption_request(self) -> None:
)
await self.handle_encryption_response(event)
+ async def read_factory_clicked(self, event: Event[bytearray]) -> None:
+ """Read factory_clicked event from client. Raise as `factory_clicked->server`."""
+ buffer = Buffer(event.data)
+
+ factory_id = buffer.read_value(StructFormat.UBYTE)
+ tile_color = Tile(buffer.read_value(StructFormat.UBYTE))
+
+ await self.raise_event(
+ Event(
+ "factory_clicked->server",
+ (
+ self.client_id,
+ factory_id,
+ tile_color,
+ ),
+ ),
+ )
+
+ async def read_cursor_location(self, event: Event[bytearray]) -> None:
+ """Read factory_clicked event from client. Raise as `factory_clicked->server`."""
+ print(f"read_cursor_location {event.data = }")
+
+ async def read_pattern_row_clicked(self, event: Event[bytearray]) -> None:
+ """Read pattern_row_clicked event from client. Raise as `pattern_row_clicked->server`."""
+ buffer = Buffer(event.data)
+
+ row_id = buffer.read_value(StructFormat.UBYTE)
+ row_pos_x = buffer.read_value(StructFormat.UBYTE)
+ row_pos_y = buffer.read_value(StructFormat.UBYTE)
+
+ await self.raise_event(
+ Event(
+ "pattern_row_clicked->server",
+ (
+ self.client_id,
+ row_id,
+ (row_pos_x, row_pos_y),
+ ),
+ ),
+ )
+
async def handle_callback_ping(
self,
_: Event[None],
@@ -138,7 +192,7 @@ async def handle_callback_ping(
"""Reraise as server[write]->callback_ping."""
await self.write_callback_ping()
- async def handle_initial_config(
+ async def write_factory_clicked(
self,
event: Event[tuple[bool, int, int, int]],
) -> None:
@@ -154,7 +208,7 @@ async def handle_initial_config(
await self.write_event(Event("server[write]->initial_config", buffer))
- async def handle_playing_as(
+ async def write_playing_as(
self,
event: Event[int],
) -> None:
@@ -165,7 +219,7 @@ async def handle_playing_as(
buffer.write_value(StructFormat.UBYTE, playing_as)
await self.write_event(Event("server[write]->playing_as", buffer))
- async def handle_game_over(self, event: Event[int]) -> None:
+ async def write_game_over(self, event: Event[int]) -> None:
"""Read game over event and reraise as server[write]->game_over."""
winner = event.data
@@ -175,7 +229,7 @@ async def handle_game_over(self, event: Event[int]) -> None:
await self.write_event(Event("server[write]->game_over", buffer))
- async def handle_board_data(
+ async def write_board_data(
self,
event: Event[tuple[int, NDArray[int8]]],
) -> None:
@@ -188,7 +242,7 @@ async def handle_board_data(
await self.write_event(Event("server[write]->board_data", buffer))
- async def handle_factory_data(
+ async def write_factory_data(
self,
event: Event[tuple[int, Counter[int]]],
) -> None:
@@ -201,6 +255,53 @@ async def handle_factory_data(
await self.write_event(Event("server[write]->factory_data", buffer))
+ async def write_cursor_data(
+ self,
+ event: Event[Counter[int]],
+ ) -> None:
+ """Reraise as server[write]->cursor_data."""
+ tiles = event.data
+
+ buffer = encode_numeric_uint8_counter(tiles)
+
+ await self.write_event(Event("server[write]->cursor_data", buffer))
+
+ async def write_table_data(
+ self,
+ event: Event[Counter[int]],
+ ) -> None:
+ """Reraise as server[write]->table_data."""
+ tiles = event.data
+
+ buffer = encode_numeric_uint8_counter(tiles)
+
+ await self.write_event(Event("server[write]->table_data", buffer))
+
+ async def write_cursor_movement_mode(
+ self,
+ event: Event[bool],
+ ) -> None:
+ """Reraise as server[write]->table_data."""
+ client_mode = event.data
+
+ buffer = Buffer()
+ buffer.write_value(StructFormat.BOOL, client_mode)
+
+ await self.write_event(
+ Event("server[write]->cursor_movement_mode", buffer),
+ )
+
+
+class ServerPlayer(IntEnum):
+ """Server Player enum."""
+
+ one = 0
+ two = auto()
+ three = auto()
+ four = auto()
+ singleplayer_all = auto()
+ spectator = auto()
+
class GameServer(network.Server):
"""Azul server.
@@ -243,6 +344,8 @@ def bind_handlers(self) -> None:
"server_start": self.start_server,
"network_stop": self.stop_server,
"server_send_game_start": self.handle_server_start_new_game,
+ "factory_clicked->server": self.handle_client_factory_clicked,
+ "pattern_row_clicked->server": self.handle_client_pattern_row_clicked,
},
)
@@ -331,9 +434,9 @@ def setup_teams_internal(client_ids: list[int]) -> dict[int, int]:
players: dict[int, int] = {}
for idx, client_id in enumerate(client_ids):
if idx == 0:
- players[client_id] = 2
+ players[client_id] = ServerPlayer.singleplayer_all
else:
- players[client_id] = 0xFF # Spectator
+ players[client_id] = ServerPlayer.spectator
return players
@staticmethod
@@ -342,9 +445,9 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]:
players: dict[int, int] = {}
for idx, client_id in enumerate(client_ids):
if idx < 4:
- players[client_id] = idx % 4
+ players[client_id] = ServerPlayer(idx % 4)
else:
- players[client_id] = 0xFF # Spectator
+ players[client_id] = ServerPlayer.spectator
return players
def new_game_init(self, varient_play: bool = False) -> None:
@@ -465,6 +568,25 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
),
),
)
+ # Transmit table center data
+ await self.raise_event(
+ Event(
+ "table_data->network",
+ self.state.table_center,
+ ),
+ )
+
+ rev_map = {v: k for k, v in self.client_players.items()}
+ if self.internal_singleplayer_mode:
+ client_id = rev_map[ServerPlayer.singleplayer_all]
+ else:
+ client_id = rev_map[ServerPlayer(self.state.current_turn)]
+ await self.raise_event(
+ Event(
+ f"cursor_movement_mode->network[{client_id}]",
+ True,
+ ),
+ )
await self.transmit_playing_as()
@@ -615,6 +737,104 @@ async def handler(self, stream: trio.SocketStream) -> None:
self.client_count -= 1
# ServerClient's `with` block handles closing stream.
+ async def handle_client_factory_clicked(
+ self,
+ event: Event[tuple[int, int, Tile]],
+ ) -> None:
+ """Handle client clicked a factory tile."""
+ if not self.players_can_interact:
+ print("Players are not allowed to interact.")
+ await trio.lowlevel.checkpoint()
+ return
+
+ client_id, factory_id, tile = event.data
+
+ server_player_id = self.client_players[client_id]
+
+ if server_player_id == ServerPlayer.spectator:
+ print(f"Spectator cannot select {factory_id = } {tile}")
+ await trio.lowlevel.checkpoint()
+ return
+
+ player_id = int(server_player_id)
+ if server_player_id == ServerPlayer.singleplayer_all:
+ player_id = self.state.current_turn
+
+ if player_id != self.state.current_turn:
+ print(
+ "Player {player_id} (client ID {client_id}) cannot select factory tile, not their turn.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if self.state.current_phase != Phase.factory_offer:
+ print(
+ "Player {player_id} (client ID {client_id}) cannot select factory tile, not in factory offer phase.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ factory_display = self.state.factory_displays.get(factory_id)
+ if factory_display is None:
+ print(
+ "Player {player_id} (client ID {client_id}) cannot select invalid factory {factory_id!r}.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if tile < 0 or tile not in factory_display:
+ print(
+ "Player {player_id} (client ID {client_id}) cannot select nonexistent color {tile}.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if not self.state.can_cursor_select_factory_color(
+ factory_id,
+ int(tile),
+ ):
+ print(
+ "Player {player_id} (client ID {client_id}) cannot select factory tile, state says no.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ # Perform move
+ self.state = self.state.cursor_selects_factory(factory_id, int(tile))
+
+ # Send updates to client
+ # Send factory display changes
+ await self.raise_event(
+ Event(
+ "factory_data->network",
+ (
+ factory_id,
+ self.state.factory_displays[factory_id],
+ ),
+ ),
+ )
+ await self.raise_event(
+ Event(
+ "cursor_data->network",
+ self.state.cursor_contents,
+ ),
+ )
+ await self.raise_event(
+ Event(
+ "table_data->network",
+ self.state.table_center,
+ ),
+ )
+
+ async def handle_client_pattern_row_clicked(
+ self,
+ event: Event[tuple[int, int, tuple[int, int]]],
+ ) -> None:
+ """Handle client clicking on pattern row."""
+ client_id, row_id, row_pos = event.data
+ print(f"handle_client_pattern_row_clicked {event.data = }")
+ await trio.lowlevel.checkpoint()
+
def __del__(self) -> None:
"""Debug print."""
print(f"del {self.__class__.__name__}")
diff --git a/src/azul/state.py b/src/azul/state.py
index 0041124..57bbe12 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -684,7 +684,7 @@ def new_game(cls, player_count: int, varient_play: bool = False) -> Self:
current_phase=Phase.factory_offer,
bag=bag,
box_lid=Counter(),
- table_center=Counter({Tile.one, 1}),
+ table_center=Counter({Tile.one: 1}),
factory_displays=factory_displays,
cursor_contents=Counter(),
current_turn=0,
diff --git a/src/azul/vector.py b/src/azul/vector.py
index 5302e6d..57d50a0 100644
--- a/src/azul/vector.py
+++ b/src/azul/vector.py
@@ -158,6 +158,12 @@ def __round__(
"""Return result of rounding self components to given number of digits."""
return self.rounded(ndigits)
+ def floored(
+ self: Self,
+ ) -> Self:
+ """Return result of rounding self components to given number of digits."""
+ return self.from_iter(int(c) for c in self)
+
def __abs__(
self: Self,
) -> Self:
From 1d8fe1768ca340037366f6bfd346d25df8f1f950 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 30 Nov 2024 23:28:44 -0700
Subject: [PATCH 15/67] WIP pattern line handling
---
src/azul/server.py | 45 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 44 insertions(+), 1 deletion(-)
diff --git a/src/azul/server.py b/src/azul/server.py
index 6e1f2f7..b8554ea 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -831,8 +831,51 @@ async def handle_client_pattern_row_clicked(
event: Event[tuple[int, int, tuple[int, int]]],
) -> None:
"""Handle client clicking on pattern row."""
+ if not self.players_can_interact:
+ print("Players are not allowed to interact.")
+ await trio.lowlevel.checkpoint()
+ return
+
client_id, row_id, row_pos = event.data
- print(f"handle_client_pattern_row_clicked {event.data = }")
+
+ server_player_id = self.client_players[client_id]
+
+ if server_player_id == ServerPlayer.spectator:
+ print(f"Spectator cannot select {row_id = } {row_pos}")
+ await trio.lowlevel.checkpoint()
+ return
+
+ player_id = int(server_player_id)
+ if server_player_id == ServerPlayer.singleplayer_all:
+ player_id = self.state.current_turn
+
+ if player_id != self.state.current_turn:
+ print(
+ "Player {player_id} (client ID {client_id}) cannot select pattern row, not their turn.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if self.state.current_phase != Phase.factory_offer:
+ print(
+ "Player {player_id} (client ID {client_id}) cannot select pattern row, not in factory offer phase.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if player_id != row_id:
+ print(
+ "Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} that does not belong to them.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ column, line_id = row_pos
+ place_count = 5 - column
+
+ print(
+ f"handle_client_pattern_row_clicked {line_id = } {place_count = }",
+ )
await trio.lowlevel.checkpoint()
def __del__(self) -> None:
From 06ce6bac91ed47215f0bdaa302bda6b5abfd48be Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Tue, 3 Dec 2024 01:18:29 -0600
Subject: [PATCH 16/67] Update requirements
---
.pre-commit-config.yaml | 2 +-
test-requirements.txt | 21 +++++++++++++--------
2 files changed, 14 insertions(+), 9 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f030e52..66ef42b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -23,7 +23,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.8.0
+ rev: v0.8.1
hooks:
- id: ruff
types: [file]
diff --git a/test-requirements.txt b/test-requirements.txt
index 74f1709..adb56ab 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -18,12 +18,12 @@ colorama==0.4.6 ; (implementation_name != 'cpython' and sys_platform == 'win32')
# via
# click
# pytest
-coverage==7.6.4
+coverage==7.6.8
# via
# -r test-requirements.in
# pytest-cov
-cryptography==43.0.3
- # via -r test-requirements.in
+cryptography==44.0.0
+ # via libcomponent
exceptiongroup==1.2.2 ; python_full_version < '3.11'
# via
# -r test-requirements.in
@@ -33,16 +33,19 @@ idna==3.10
# via trio
iniconfig==2.0.0
# via pytest
+libcomponent @ git+https://github.com/CoolCat467/LibComponent@b06eaca2f029a9fc019d68194ade260ccb00a841
+ # via -r test-requirements.in
mypy==1.13.0
# via -r test-requirements.in
mypy-extensions==1.0.0
# via
# -r test-requirements.in
# black
+ # libcomponent
# mypy
numpy==2.1.3
# via -r test-requirements.in
-orjson==3.10.11 ; implementation_name == 'cpython'
+orjson==3.10.12 ; implementation_name == 'cpython'
# via -r test-requirements.in
outcome==1.3.0.post0
# via
@@ -62,7 +65,7 @@ pycparser==2.22 ; (os_name != 'nt' and platform_python_implementation != 'PyPy')
# via cffi
pygame==2.6.1
# via -r test-requirements.in
-pytest==8.3.3
+pytest==8.3.4
# via
# -r test-requirements.in
# pytest-cov
@@ -71,13 +74,13 @@ pytest-cov==6.0.0
# via -r test-requirements.in
pytest-trio==0.8.0
# via -r test-requirements.in
-ruff==0.7.3
+ruff==0.8.1
# via -r test-requirements.in
sniffio==1.3.1
# via trio
sortedcontainers==2.4.0
# via trio
-tomli==2.0.2 ; python_full_version <= '3.11'
+tomli==2.2.1 ; python_full_version <= '3.11'
# via
# black
# coverage
@@ -86,11 +89,13 @@ tomli==2.0.2 ; python_full_version <= '3.11'
trio==0.27.0
# via
# -r test-requirements.in
+ # libcomponent
# pytest-trio
typing-extensions==4.12.2
# via
# -r test-requirements.in
# black
+ # libcomponent
# mypy
-uv==0.5.1
+uv==0.5.5
# via -r test-requirements.in
From 03b51eb0146d9f98a039a69fb6d25411f5a6922a Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Tue, 3 Dec 2024 01:20:30 -0600
Subject: [PATCH 17/67] Fix encoding and implement decoding for cursor location
---
src/azul/client.py | 7 +++----
src/azul/game.py | 2 +-
src/azul/server.py | 25 ++++++++++++++++++++++++-
3 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index 2d9be7c..7421cdd 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -432,10 +432,9 @@ async def write_game_cursor_location_transmit(
"""Write cursor_location_transmit event to server."""
scaled_location = event.data
- x, y = map(int, (scaled_location * 4096).rounded()) # 2 ** 12
-
- position = (x & 0xFFF) << 3 | (y & 0xFFF)
- buffer = position.to_bytes(3, "big")
+ x, y = map(int, (scaled_location * 0xFFF).floored())
+ position = ((x & 0xFFF) << 12) | (y & 0xFFF)
+ buffer = (position & 0xFFFFFF).to_bytes(3)
await self.raise_event(Event("cursor_location->server[write]", buffer))
diff --git a/src/azul/game.py b/src/azul/game.py
index b784c8c..23b8018 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -567,7 +567,7 @@ async def handle_pygame_mouse_motion(
self.location = event.data["pos"]
##transmit_location = Vector2.from_iter(
- ## x / y for x, y in zip(self.location, SCREEN_SIZE, strict=False)
+ ## x / y for x, y in zip(self.location, SCREEN_SIZE, strict=True)
##)
##
### Transmit to server
diff --git a/src/azul/server.py b/src/azul/server.py
index b8554ea..3096bda 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -164,7 +164,19 @@ async def read_factory_clicked(self, event: Event[bytearray]) -> None:
async def read_cursor_location(self, event: Event[bytearray]) -> None:
"""Read factory_clicked event from client. Raise as `factory_clicked->server`."""
- print(f"read_cursor_location {event.data = }")
+ buffer = int.from_bytes(event.data) & 0xFFFFFF
+ x = (buffer >> 12) & 0xFFF
+ y = buffer & 0xFFF
+
+ await self.raise_event(
+ Event(
+ "cursor_location->server",
+ (
+ self.client_id,
+ (x, y),
+ ),
+ ),
+ )
async def read_pattern_row_clicked(self, event: Event[bytearray]) -> None:
"""Read pattern_row_clicked event from client. Raise as `pattern_row_clicked->server`."""
@@ -346,6 +358,7 @@ def bind_handlers(self) -> None:
"server_send_game_start": self.handle_server_start_new_game,
"factory_clicked->server": self.handle_client_factory_clicked,
"pattern_row_clicked->server": self.handle_client_pattern_row_clicked,
+ "cursor_location->server": self.handle_cursor_location,
},
)
@@ -878,6 +891,16 @@ async def handle_client_pattern_row_clicked(
)
await trio.lowlevel.checkpoint()
+ async def handle_cursor_location(
+ self,
+ event: Event[tuple[int, tuple[int, int]]],
+ ) -> None:
+ """Handle cursor location sent from client."""
+ client_id, pos = event.data
+
+ print(f"handle_cursor_location {client_id = } {pos = }")
+ await trio.lowlevel.checkpoint()
+
def __del__(self) -> None:
"""Debug print."""
print(f"del {self.__class__.__name__}")
From 077f67d140eb7339f0f12d6118acb8b133701eac Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 13 Dec 2024 10:59:54 -0600
Subject: [PATCH 18/67] Work on starting to send cursor location
---
src/azul/game.py | 7 +++++++
src/azul/server.py | 23 +++++++++++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/src/azul/game.py b/src/azul/game.py
index 23b8018..7749978 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -39,6 +39,7 @@
import pygame
import trio
from libcomponent.component import (
+ Component,
ComponentManager,
Event,
ExternalRaiseManager,
@@ -464,6 +465,12 @@ def get_tile_point(
return tile_position.floored()
+class EventClock(Component):
+ """Event Clock Component."""
+
+ __slots__ = ()
+
+
class Cursor(TileRenderer):
"""Cursor TileRenderer.
diff --git a/src/azul/server.py b/src/azul/server.py
index 3096bda..891927a 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -896,8 +896,31 @@ async def handle_cursor_location(
event: Event[tuple[int, tuple[int, int]]],
) -> None:
"""Handle cursor location sent from client."""
+ if not self.players_can_interact:
+ print("Players are not allowed to interact.")
+ await trio.lowlevel.checkpoint()
+ return
+
client_id, pos = event.data
+ server_player_id = self.client_players[client_id]
+
+ if server_player_id == ServerPlayer.spectator:
+ print(f"Spectator cannot select {pos = }")
+ await trio.lowlevel.checkpoint()
+ return
+
+ player_id = int(server_player_id)
+ if server_player_id == ServerPlayer.singleplayer_all:
+ player_id = self.state.current_turn
+
+ if player_id != self.state.current_turn:
+ print(
+ "Player {player_id} (client ID {client_id}) cannot move cursor, not their turn.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
print(f"handle_cursor_location {client_id = } {pos = }")
await trio.lowlevel.checkpoint()
From c7ea408f9b4df73912dac6eb16d220ca277b9ac8 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 13 Dec 2024 11:00:24 -0600
Subject: [PATCH 19/67] Add `zizmor` pre-commit hook
---
.github/workflows/autodeps.yml | 2 ++
.github/workflows/ci.yml | 2 ++
.pre-commit-config.yaml | 6 +++++-
3 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml
index 757f6c8..5918fd9 100644
--- a/.github/workflows/autodeps.yml
+++ b/.github/workflows/autodeps.yml
@@ -20,6 +20,8 @@ jobs:
steps:
- name: Checkout
+ with:
+ persist-credentials: false
uses: actions/checkout@v4
- name: Setup python
uses: actions/setup-python@v5
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fc9b89d..b6bbbe7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -78,6 +78,8 @@ jobs:
}}
steps:
- name: Checkout
+ with:
+ persist-credentials: false
uses: actions/checkout@v4
- name: Setup python
uses: actions/setup-python@v5
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 66ef42b..e9fafbc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -23,7 +23,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.8.1
+ rev: v0.8.3
hooks:
- id: ruff
types: [file]
@@ -36,6 +36,10 @@ repos:
rev: v2.3.0
hooks:
- id: codespell
+ - repo: https://github.com/woodruffw/zizmor-pre-commit
+ rev: v0.9.1
+ hooks:
+ - id: zizmor
- repo: local
hooks:
- id: project-requirements
From d5a0839ba6f6ac3161a097b015c4143ea96c5016 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Mon, 16 Dec 2024 22:53:49 -0600
Subject: [PATCH 20/67] Transmit pattern data and current turn changes
---
.pre-commit-config.yaml | 2 +-
src/azul/client.py | 26 ++++++++
src/azul/game.py | 36 +++++++++-
src/azul/network_shared.py | 2 +
src/azul/server.py | 132 +++++++++++++++++++++++++++++++++++--
src/azul/state.py | 2 +-
test-requirements.txt | 8 +--
7 files changed, 196 insertions(+), 12 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e9fafbc..ff6904d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -37,7 +37,7 @@ repos:
hooks:
- id: codespell
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v0.9.1
+ rev: v0.9.2
hooks:
- id: zizmor
- repo: local
diff --git a/src/azul/client.py b/src/azul/client.py
index 7421cdd..414beeb 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -42,6 +42,7 @@
ServerBoundEvents,
decode_int8_array,
decode_numeric_uint8_counter,
+ decode_tile_count,
)
if TYPE_CHECKING:
@@ -182,10 +183,12 @@ def __init__(self, name: str) -> None:
cbe.playing_as: "server->playing_as",
cbe.game_over: "server->game_over",
cbe.board_data: "server->board_data",
+ cbe.pattern_data: "server->pattern_data",
cbe.factory_data: "server->factory_data",
cbe.cursor_data: "server->cursor_data",
cbe.table_data: "server->table_data",
cbe.cursor_movement_mode: "server->cursor_movement_mode",
+ cbe.current_turn_change: "server->current_turn_change",
},
)
@@ -203,10 +206,12 @@ def bind_handlers(self) -> None:
"server->playing_as": self.read_playing_as,
"server->game_over": self.read_game_over,
"server->board_data": self.read_board_data,
+ "server->pattern_data": self.read_pattern_data,
"server->factory_data": self.read_factory_data,
"server->cursor_data": self.read_cursor_data,
"server->table_data": self.read_table_data,
"server->cursor_movement_mode": self.read_cursor_movement_mode,
+ "server->current_turn_change": self.read_current_turn_change,
"client_connect": self.handle_client_connect,
"network_stop": self.handle_network_stop,
"game_factory_clicked": self.write_game_factory_clicked,
@@ -377,6 +382,18 @@ async def read_board_data(self, event: Event[bytearray]) -> None:
await self.raise_event(Event("game_board_data", (player_id, array)))
+ async def read_pattern_data(self, event: Event[bytearray]) -> None:
+ """Read pattern_data event from server, reraise as `game_pattern_data`."""
+ buffer = Buffer(event.data)
+
+ player_id: u8 = buffer.read_value(StructFormat.UBYTE)
+ row_id: u8 = buffer.read_value(StructFormat.UBYTE)
+ tile_data = decode_tile_count(buffer)
+
+ await self.raise_event(
+ Event("game_pattern_data", (player_id, row_id, tile_data)),
+ )
+
async def read_factory_data(self, event: Event[bytearray]) -> None:
"""Read factory_data event from server, reraise as `game_factory_data`."""
buffer = Buffer(event.data)
@@ -412,6 +429,15 @@ async def read_cursor_movement_mode(self, event: Event[bytearray]) -> None:
Event("game_cursor_set_movement_mode", client_mode),
)
+ async def read_current_turn_change(self, event: Event[bytearray]) -> None:
+ """Read current_turn_change event from server, reraise as `game_pattern_current_turn_change`."""
+ buffer = Buffer(event.data)
+
+ pattern_id: u8 = buffer.read_value(StructFormat.UBYTE)
+ await self.raise_event(
+ Event("game_pattern_current_turn_change", pattern_id),
+ )
+
async def write_game_factory_clicked(
self,
event: Event[tuple[int, Tile]],
diff --git a/src/azul/game.py b/src/azul/game.py
index 7749978..52ac281 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -840,8 +840,14 @@ def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.rows_id})"
def bind_handlers(self) -> None:
- """Register click event handler."""
- self.register_handler("click", self.handle_click)
+ """Register event handlers."""
+ self.register_handlers(
+ {
+ "click": self.handle_click,
+ "game_pattern_current_turn_change": self.handle_game_pattern_current_turn_change,
+ "game_pattern_data": self.handle_game_pattern_data,
+ },
+ )
def update_image(self) -> None:
"""Update self.image."""
@@ -906,6 +912,32 @@ async def handle_click(
),
)
+ async def handle_game_pattern_current_turn_change(
+ self,
+ event: Event[int],
+ ) -> None:
+ """Handle game_pattern_current_turn_change event."""
+ player_id = event.data
+
+ if player_id == self.rows_id:
+ self.set_background(DARKGREEN)
+ else:
+ self.set_background(None)
+
+ async def handle_game_pattern_data(
+ self,
+ event: Event[tuple[int, int, tuple[int, int]]],
+ ) -> None:
+ """Handle game_pattern_data event."""
+ player_id, row_id, (raw_tile_color, tile_count) = event.data
+
+ if player_id != self.rows_id:
+ await trio.lowlevel.checkpoint()
+ return
+ tile_color = Tile(raw_tile_color)
+ self.set_row_data(row_id, tile_color, tile_count)
+ await trio.lowlevel.checkpoint()
+
class FloorLine(Row):
"""Represents a player's floor line."""
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index 84168ef..149d732 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -117,10 +117,12 @@ class ClientBoundEvents(IntEnum):
playing_as = auto()
game_over = auto()
board_data = auto()
+ pattern_data = auto()
factory_data = auto()
cursor_data = auto()
table_data = auto()
cursor_movement_mode = auto()
+ current_turn_change = auto()
class ServerBoundEvents(IntEnum):
diff --git a/src/azul/server.py b/src/azul/server.py
index 891927a..66e526d 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -55,6 +55,7 @@
ServerBoundEvents,
encode_int8_array,
encode_numeric_uint8_counter,
+ encode_tile_count,
)
from azul.state import Phase, State, Tile
@@ -96,10 +97,12 @@ def __init__(self, client_id: int) -> None:
"server[write]->playing_as": cbe.playing_as,
"server[write]->game_over": cbe.game_over,
"server[write]->board_data": cbe.board_data,
+ "server[write]->pattern_data": cbe.pattern_data,
"server[write]->factory_data": cbe.factory_data,
"server[write]->cursor_data": cbe.cursor_data,
"server[write]->table_data": cbe.table_data,
"server[write]->cursor_movement_mode": cbe.cursor_movement_mode,
+ "server[write]->current_turn_change": cbe.current_turn_change,
},
)
sbe = ServerBoundEvents
@@ -130,6 +133,8 @@ def bind_handlers(self) -> None:
"cursor_data->network": self.write_cursor_data,
"table_data->network": self.write_table_data,
f"cursor_movement_mode->network[{self.client_id}]": self.write_cursor_movement_mode,
+ "current_turn_change->network": self.write_current_turn_change,
+ "pattern_data->network": self.write_pattern_data,
},
)
@@ -293,7 +298,7 @@ async def write_cursor_movement_mode(
self,
event: Event[bool],
) -> None:
- """Reraise as server[write]->table_data."""
+ """Reraise as server[write]->cursor_movement_mode."""
client_mode = event.data
buffer = Buffer()
@@ -303,6 +308,37 @@ async def write_cursor_movement_mode(
Event("server[write]->cursor_movement_mode", buffer),
)
+ async def write_current_turn_change(
+ self,
+ event: Event[int],
+ ) -> None:
+ """Reraise as server[write]->current_turn_change."""
+ pattern_id = event.data
+
+ buffer = Buffer()
+ buffer.write_value(StructFormat.UBYTE, pattern_id)
+
+ await self.write_event(
+ Event("server[write]->current_turn_change", buffer),
+ )
+
+ async def write_pattern_data(
+ self,
+ event: Event[tuple[int, int, tuple[int, int]]],
+ ) -> None:
+ """Reraise as server[write]->board_data."""
+ player_id, row_id, (tile_color, tile_count) = event.data
+
+ buffer = Buffer()
+ buffer.write_value(StructFormat.UBYTE, player_id)
+ buffer.write_value(StructFormat.UBYTE, row_id)
+ assert tile_color >= 0
+ buffer.extend(encode_tile_count(tile_color, tile_count))
+
+ await self.write_event(
+ Event("server[write]->pattern_data", buffer),
+ )
+
class ServerPlayer(IntEnum):
"""Server Player enum."""
@@ -750,6 +786,35 @@ async def handler(self, stream: trio.SocketStream) -> None:
self.client_count -= 1
# ServerClient's `with` block handles closing stream.
+ def find_client_id_from_server_player_id(
+ self,
+ server_player_id: ServerPlayer,
+ ) -> int | None:
+ """Return client id from server player id or None if not found."""
+ for client_id, current_server_player_id in self.client_players.items():
+ if current_server_player_id == server_player_id:
+ return client_id
+ # Return singleplayer client id if exists
+ if current_server_player_id == ServerPlayer.singleplayer_all:
+ return client_id
+ return None
+
+ def find_server_player_id_from_state_turn(
+ self,
+ state_turn: int,
+ ) -> ServerPlayer:
+ """Return ServerPlayer id from game state turn."""
+ if self.internal_singleplayer_mode:
+ return ServerPlayer.singleplayer_all
+ return ServerPlayer(state_turn)
+
+ def find_client_id_from_state_turn(self, state_turn: int) -> int | None:
+ """Return client id from state turn or None if not found."""
+ server_player_id = self.find_server_player_id_from_state_turn(
+ state_turn,
+ )
+ return self.find_client_id_from_server_player_id(server_player_id)
+
async def handle_client_factory_clicked(
self,
event: Event[tuple[int, int, Tile]],
@@ -886,10 +951,69 @@ async def handle_client_pattern_row_clicked(
column, line_id = row_pos
place_count = 5 - column
- print(
- f"handle_client_pattern_row_clicked {line_id = } {place_count = }",
+ color = self.state.get_cursor_holding_color()
+ if not self.state.can_player_select_line(line_id, color, place_count):
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select pattern line {line_id} placing {place_count} {Tile(color)} tiles.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ prev_player_turn = self.state.current_turn
+
+ self.state = self.state.player_selects_pattern_line(
+ line_id,
+ place_count,
+ )
+
+ if self.state.current_turn != player_id:
+ if server_player_id != ServerPlayer.singleplayer_all:
+ new_client_id = self.find_client_id_from_state_turn(
+ self.state.current_turn,
+ )
+ assert new_client_id is not None
+ await self.raise_event(
+ Event(
+ f"cursor_movement_mode->network[{client_id}]",
+ False,
+ ),
+ )
+ await self.raise_event(
+ Event(
+ f"cursor_movement_mode->network[{new_client_id}]",
+ True,
+ ),
+ )
+
+ await self.raise_event(
+ Event(
+ "current_turn_change->network",
+ self.state.current_turn,
+ ),
+ )
+
+ raw_tile_color, tile_count = self.state.player_data[
+ prev_player_turn
+ ].lines[line_id]
+ # Do not send blank colors, clamp to zero
+ tile_color = max(0, int(raw_tile_color))
+ await self.raise_event(
+ Event(
+ "pattern_data->network",
+ (
+ prev_player_turn,
+ line_id,
+ (tile_color, tile_count),
+ ),
+ ),
+ )
+
+ await self.raise_event(
+ Event(
+ "cursor_data->network",
+ self.state.cursor_contents,
+ ),
)
- await trio.lowlevel.checkpoint()
async def handle_cursor_location(
self,
diff --git a/src/azul/state.py b/src/azul/state.py
index 57bbe12..6018c0f 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -1165,8 +1165,8 @@ def preform_action(
raise NotImplementedError()
def _manual_wall_tiling_maybe_next_turn(self) -> Self:
+ # return self
raise NotImplementedError()
- return self
def get_manual_wall_tiling_locations_for_player(
self,
diff --git a/test-requirements.txt b/test-requirements.txt
index adb56ab..904fa0f 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,6 +1,6 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --universal --python-version=3.10 test-requirements.in -o test-requirements.txt
-attrs==24.2.0
+attrs==24.3.0
# via
# outcome
# trio
@@ -18,7 +18,7 @@ colorama==0.4.6 ; (implementation_name != 'cpython' and sys_platform == 'win32')
# via
# click
# pytest
-coverage==7.6.8
+coverage==7.6.9
# via
# -r test-requirements.in
# pytest-cov
@@ -74,7 +74,7 @@ pytest-cov==6.0.0
# via -r test-requirements.in
pytest-trio==0.8.0
# via -r test-requirements.in
-ruff==0.8.1
+ruff==0.8.3
# via -r test-requirements.in
sniffio==1.3.1
# via trio
@@ -97,5 +97,5 @@ typing-extensions==4.12.2
# black
# libcomponent
# mypy
-uv==0.5.5
+uv==0.5.9
# via -r test-requirements.in
From 46e05f1bcc0dbe0bfb268685c013551135139885 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Tue, 17 Dec 2024 03:26:18 -0600
Subject: [PATCH 21/67] If user clicks row, probably intended to place tiles.
---
src/azul/server.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/azul/server.py b/src/azul/server.py
index 66e526d..2bbe106 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -952,6 +952,11 @@ async def handle_client_pattern_row_clicked(
place_count = 5 - column
color = self.state.get_cursor_holding_color()
+
+ max_place = self.state.get_player_line_max_placable_count(line_id)
+ current_hold_count = self.state.cursor_contents[color]
+ place_count = min(place_count, current_hold_count, max_place)
+
if not self.state.can_player_select_line(line_id, color, place_count):
print(
f"Player {player_id} (client ID {client_id}) cannot select pattern line {line_id} placing {place_count} {Tile(color)} tiles.",
From ea9d971887fe485f35a57a765d3d1ff26d7bf4f8 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Tue, 17 Dec 2024 22:31:54 -0600
Subject: [PATCH 22/67] Transmit cursor location
---
src/azul/client.py | 18 +++++-
src/azul/game.py | 124 ++++++++++++++++++++++++++++++-------
src/azul/network_shared.py | 17 +++++
src/azul/server.py | 92 ++++++++++++++++++++-------
src/azul/sprite.py | 16 ++---
test-requirements.txt | 10 +--
6 files changed, 214 insertions(+), 63 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index 414beeb..e2fe064 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -40,16 +40,18 @@
ADVERTISEMENT_PORT,
ClientBoundEvents,
ServerBoundEvents,
+ decode_cursor_location,
decode_int8_array,
decode_numeric_uint8_counter,
decode_tile_count,
+ encode_cursor_location,
)
+from azul.vector import Vector2
if TYPE_CHECKING:
from mypy_extensions import u8
from azul.state import Tile
- from azul.vector import Vector2
async def read_advertisements(
@@ -189,6 +191,7 @@ def __init__(self, name: str) -> None:
cbe.table_data: "server->table_data",
cbe.cursor_movement_mode: "server->cursor_movement_mode",
cbe.current_turn_change: "server->current_turn_change",
+ cbe.cursor_position: "server->cursor_position",
},
)
@@ -212,6 +215,7 @@ def bind_handlers(self) -> None:
"server->table_data": self.read_table_data,
"server->cursor_movement_mode": self.read_cursor_movement_mode,
"server->current_turn_change": self.read_current_turn_change,
+ "server->cursor_position": self.read_cursor_position,
"client_connect": self.handle_client_connect,
"network_stop": self.handle_network_stop,
"game_factory_clicked": self.write_game_factory_clicked,
@@ -438,6 +442,15 @@ async def read_current_turn_change(self, event: Event[bytearray]) -> None:
Event("game_pattern_current_turn_change", pattern_id),
)
+ async def read_cursor_position(self, event: Event[bytearray]) -> None:
+ """Read current_turn_change event from server, reraise as `game_cursor_set_destination`."""
+ location = decode_cursor_location(event.data)
+ unit_location = Vector2.from_iter(x / 0xFFF for x in location)
+
+ await self.raise_event(
+ Event("game_cursor_set_destination", unit_location),
+ )
+
async def write_game_factory_clicked(
self,
event: Event[tuple[int, Tile]],
@@ -459,8 +472,7 @@ async def write_game_cursor_location_transmit(
scaled_location = event.data
x, y = map(int, (scaled_location * 0xFFF).floored())
- position = ((x & 0xFFF) << 12) | (y & 0xFFF)
- buffer = (position & 0xFFFFFF).to_bytes(3)
+ buffer = encode_cursor_location((x, y))
await self.raise_event(Event("cursor_location->server[write]", buffer))
diff --git a/src/azul/game.py b/src/azul/game.py
index 52ac281..25ae555 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -466,9 +466,47 @@ def get_tile_point(
class EventClock(Component):
- """Event Clock Component."""
+ """Event Clock Component.
- __slots__ = ()
+ Will raise `self.event_to_raise` every `self.duration` seconds.
+ If more than duration seconds pass before ticks, will only raise
+ one event.
+
+ Do not pass leveled events, event reference is maintained and
+ when first event is mutated with pop_level it will be same object
+ and only first run will be leveled event.
+ """
+
+ __slots__ = ("duration", "event_to_raise", "time_passed")
+
+ def __init__(
+ self,
+ name: str,
+ duration: float,
+ event_to_raise: Event[Any],
+ ) -> None:
+ """Initialize with name, duration, and event to raise."""
+ super().__init__(name)
+
+ self.time_passed: float = 0.0
+ self.duration = duration
+ self.event_to_raise = event_to_raise
+
+ def bind_handlers(self) -> None:
+ """Register tick event handler."""
+ self.register_handler("tick", self.handle_tick)
+
+ async def handle_tick(self, event: Event[sprite.TickEventData]) -> None:
+ """Handle tick event."""
+ self.time_passed += event.data.time_passed
+ truediv, self.time_passed = divmod(self.time_passed, self.duration)
+
+ # Could raise multiple times, but I am deciding that we will
+ # only raise at most once even if we miss the train
+ if truediv:
+ # Known issue: Event to raise cannot be a leveled event,
+ # because event.pop_level mutates the event object in place
+ await self.raise_event(self.event_to_raise)
class Cursor(TileRenderer):
@@ -484,8 +522,14 @@ class Cursor(TileRenderer):
- PygameMouseMotion
"""
- __slots__ = ("tiles",)
+ __slots__ = (
+ "client_mode",
+ "last_transmit_pos",
+ "tiles",
+ "time_passed",
+ )
greyshift = GREYSHIFT
+ duration = 0.25
def __init__(self) -> None:
"""Initialize cursor with a game it belongs to."""
@@ -494,13 +538,16 @@ def __init__(self) -> None:
self.add_components(
(
- sprite.MovementComponent(speed=800),
+ sprite.MovementComponent(speed=600),
sprite.TargetingComponent("cursor_reached_destination"),
),
)
# Stored in reverse render order
self.tiles: list[int] = []
+ self.last_transmit_pos = self.location
+ self.time_passed = 0.0
+ self.client_mode = False
def update_image(self) -> None:
"""Update self.image."""
@@ -555,13 +602,19 @@ async def handle_cursor_set_destination(
event: Event[tuple[int, int]],
) -> None:
"""Start moving towards new destination."""
+ destination = Vector2.from_iter(
+ x * y for x, y in zip(event.data, SCREEN_SIZE, strict=True)
+ ).floored()
+ # print(f"handle_cursor_set_destination {destination = }")
+
targeting: sprite.TargetingComponent = self.get_component("targeting")
- targeting.destination = event.data
+ targeting.destination = destination
if not self.has_handler("tick"):
self.register_handler(
"tick",
- targeting.move_destination_time_ticks,
+ self.handle_tick,
)
+
self.move_to_front()
await trio.lowlevel.checkpoint()
@@ -572,34 +625,57 @@ async def handle_pygame_mouse_motion(
"""Set location to event data."""
self.move_to_front()
self.location = event.data["pos"]
+ await trio.lowlevel.checkpoint()
- ##transmit_location = Vector2.from_iter(
- ## x / y for x, y in zip(self.location, SCREEN_SIZE, strict=True)
- ##)
- ##
- ### Transmit to server
- ### Event level to so reaches client
- ##await self.raise_event(
- ## Event(
- ## "game_cursor_location_transmit",
- ## transmit_location,
- ## 2,
- ## )
- ##)
+ async def handle_tick(self, event: Event[sprite.TickEventData]) -> None:
+ """Handle tick event."""
+ if self.client_mode:
+ self.time_passed += event.data.time_passed
+ truediv, self.time_passed = divmod(self.time_passed, self.duration)
+
+ if self.last_transmit_pos != self.location and truediv:
+ self.last_transmit_pos = self.location
+ else:
+ await trio.lowlevel.checkpoint()
+ return
+
+ transmit_location = Vector2.from_iter(
+ x / y for x, y in zip(self.location, SCREEN_SIZE, strict=True)
+ )
+
+ # Transmit to server
+ # Event level to so reaches client
+ await self.raise_event(
+ Event(
+ "game_cursor_location_transmit",
+ transmit_location,
+ 2,
+ ),
+ )
+ else:
+ # Server mode
+ targeting: sprite.TargetingComponent = self.get_component(
+ "targeting",
+ )
+ await targeting.move_destination_time(event.data.time_passed)
async def handle_cursor_set_movement_mode(
self,
event: Event[bool],
) -> None:
"""Change cursor movement mode. True if client mode, False if server mode."""
- client_mode = event.data
- if client_mode:
- self.register_handler(
- "PygameMouseMotion",
- self.handle_pygame_mouse_motion,
+ self.client_mode = event.data
+ # print(f'handle_cursor_set_movement_mode {self.client_mode = }')
+ if self.client_mode:
+ self.register_handlers(
+ {
+ "PygameMouseMotion": self.handle_pygame_mouse_motion,
+ "tick": self.handle_tick,
+ },
)
else:
self.unregister_handler_type("PygameMouseMotion")
+ self.unregister_handler_type("tick")
await trio.lowlevel.checkpoint()
def get_held_count(self) -> int:
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index 149d732..79333b5 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -36,6 +36,7 @@
if TYPE_CHECKING:
from numpy.typing import NDArray
+
ADVERTISEMENT_IP: Final = "224.0.2.60"
ADVERTISEMENT_PORT: Final = 4445
@@ -108,6 +109,21 @@ def decode_int8_array(buffer: Buffer, size: tuple[int, ...]) -> NDArray[int8]:
return array
+def encode_cursor_location(scaled_location: tuple[int, int]) -> bytes:
+ """Return buffer from cursor location."""
+ x, y = scaled_location
+ position = ((x & 0xFFF) << 12) | (y & 0xFFF)
+ return (position & 0xFFFFFF).to_bytes(3)
+
+
+def decode_cursor_location(buffer: bytes | bytearray) -> tuple[int, int]:
+ """Return cursor location from buffer."""
+ value = int.from_bytes(buffer) & 0xFFFFFF
+ x = (value >> 12) & 0xFFF
+ y = value & 0xFFF
+ return (x, y)
+
+
class ClientBoundEvents(IntEnum):
"""Client bound event IDs."""
@@ -123,6 +139,7 @@ class ClientBoundEvents(IntEnum):
table_data = auto()
cursor_movement_mode = auto()
current_turn_change = auto()
+ cursor_position = auto()
class ServerBoundEvents(IntEnum):
diff --git a/src/azul/server.py b/src/azul/server.py
index 2bbe106..e5f6b51 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -53,6 +53,8 @@
DEFAULT_PORT,
ClientBoundEvents,
ServerBoundEvents,
+ decode_cursor_location,
+ encode_cursor_location,
encode_int8_array,
encode_numeric_uint8_counter,
encode_tile_count,
@@ -103,6 +105,7 @@ def __init__(self, client_id: int) -> None:
"server[write]->table_data": cbe.table_data,
"server[write]->cursor_movement_mode": cbe.cursor_movement_mode,
"server[write]->current_turn_change": cbe.current_turn_change,
+ "server[write]->cursor_position": cbe.cursor_position,
},
)
sbe = ServerBoundEvents
@@ -133,6 +136,7 @@ def bind_handlers(self) -> None:
"cursor_data->network": self.write_cursor_data,
"table_data->network": self.write_table_data,
f"cursor_movement_mode->network[{self.client_id}]": self.write_cursor_movement_mode,
+ f"cursor_position->network[{self.client_id}]": self.write_cursor_position,
"current_turn_change->network": self.write_current_turn_change,
"pattern_data->network": self.write_pattern_data,
},
@@ -169,9 +173,7 @@ async def read_factory_clicked(self, event: Event[bytearray]) -> None:
async def read_cursor_location(self, event: Event[bytearray]) -> None:
"""Read factory_clicked event from client. Raise as `factory_clicked->server`."""
- buffer = int.from_bytes(event.data) & 0xFFFFFF
- x = (buffer >> 12) & 0xFFF
- y = buffer & 0xFFF
+ x, y = decode_cursor_location(event.data)
await self.raise_event(
Event(
@@ -308,6 +310,17 @@ async def write_cursor_movement_mode(
Event("server[write]->cursor_movement_mode", buffer),
)
+ async def write_cursor_position(
+ self,
+ event: Event[tuple[int, int]],
+ ) -> None:
+ """Reraise as server[write]->cursor_position."""
+ buffer = encode_cursor_location(event.data)
+
+ await self.write_event(
+ Event("server[write]->cursor_position", buffer),
+ )
+
async def write_current_turn_change(
self,
event: Event[int],
@@ -625,11 +638,16 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
),
)
- rev_map = {v: k for k, v in self.client_players.items()}
- if self.internal_singleplayer_mode:
- client_id = rev_map[ServerPlayer.singleplayer_all]
- else:
- client_id = rev_map[ServerPlayer(self.state.current_turn)]
+ await self.transmit_cursor_movement_mode()
+
+ await self.transmit_playing_as()
+
+ async def transmit_cursor_movement_mode(self) -> None:
+ """Update current cursor movement mode for all clients."""
+ client_id = self.find_client_id_from_state_turn(
+ self.state.current_turn,
+ )
+
await self.raise_event(
Event(
f"cursor_movement_mode->network[{client_id}]",
@@ -637,7 +655,16 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
),
)
- await self.transmit_playing_as()
+ async with trio.open_nursery() as nursery:
+ for other_client_id in self.client_players:
+ if other_client_id != client_id:
+ nursery.start_soon(
+ self.raise_event,
+ Event(
+ f"cursor_movement_mode->network[{other_client_id}]",
+ False,
+ ),
+ )
async def client_network_loop(
self,
@@ -748,7 +775,9 @@ async def handler(self, stream: trio.SocketStream) -> None:
self.client_count += 1
can_start = self.can_start()
+ print(f"[azul.server] {can_start = }")
game_active = self.game_active()
+ print(f"[azul.server] {game_active = }")
# if can_start:
# self.stop_serving()
@@ -772,7 +801,8 @@ async def handler(self, stream: trio.SocketStream) -> None:
print("TODO: Joined as spectator")
# await self.send_spectator_join_packets(client)
with self.temporary_component(client):
- if can_start and not game_active and is_zee_capitan:
+ if can_start and not game_active: # and is_zee_capitan:
+ print("[azul.server] game start trigger.")
varient_play = False
await self.raise_event(
Event("server_send_game_start", varient_play),
@@ -840,14 +870,14 @@ async def handle_client_factory_clicked(
if player_id != self.state.current_turn:
print(
- "Player {player_id} (client ID {client_id}) cannot select factory tile, not their turn.",
+ f"Player {player_id} (client ID {client_id}) cannot select factory tile, not their turn.",
)
await trio.lowlevel.checkpoint()
return
if self.state.current_phase != Phase.factory_offer:
print(
- "Player {player_id} (client ID {client_id}) cannot select factory tile, not in factory offer phase.",
+ f"Player {player_id} (client ID {client_id}) cannot select factory tile, not in factory offer phase.",
)
await trio.lowlevel.checkpoint()
return
@@ -855,14 +885,14 @@ async def handle_client_factory_clicked(
factory_display = self.state.factory_displays.get(factory_id)
if factory_display is None:
print(
- "Player {player_id} (client ID {client_id}) cannot select invalid factory {factory_id!r}.",
+ f"Player {player_id} (client ID {client_id}) cannot select invalid factory {factory_id!r}.",
)
await trio.lowlevel.checkpoint()
return
if tile < 0 or tile not in factory_display:
print(
- "Player {player_id} (client ID {client_id}) cannot select nonexistent color {tile}.",
+ f"Player {player_id} (client ID {client_id}) cannot select nonexistent color {tile}.",
)
await trio.lowlevel.checkpoint()
return
@@ -872,7 +902,7 @@ async def handle_client_factory_clicked(
int(tile),
):
print(
- "Player {player_id} (client ID {client_id}) cannot select factory tile, state says no.",
+ f"Player {player_id} (client ID {client_id}) cannot select factory tile, state says no.",
)
await trio.lowlevel.checkpoint()
return
@@ -929,21 +959,21 @@ async def handle_client_pattern_row_clicked(
if player_id != self.state.current_turn:
print(
- "Player {player_id} (client ID {client_id}) cannot select pattern row, not their turn.",
+ f"Player {player_id} (client ID {client_id}) cannot select pattern row, not their turn.",
)
await trio.lowlevel.checkpoint()
return
if self.state.current_phase != Phase.factory_offer:
print(
- "Player {player_id} (client ID {client_id}) cannot select pattern row, not in factory offer phase.",
+ f"Player {player_id} (client ID {client_id}) cannot select pattern row, not in factory offer phase.",
)
await trio.lowlevel.checkpoint()
return
if player_id != row_id:
print(
- "Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} that does not belong to them.",
+ f"Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} that does not belong to them.",
)
await trio.lowlevel.checkpoint()
return
@@ -972,7 +1002,7 @@ async def handle_client_pattern_row_clicked(
)
if self.state.current_turn != player_id:
- if server_player_id != ServerPlayer.singleplayer_all:
+ if not self.internal_singleplayer_mode:
new_client_id = self.find_client_id_from_state_turn(
self.state.current_turn,
)
@@ -1030,12 +1060,12 @@ async def handle_cursor_location(
await trio.lowlevel.checkpoint()
return
- client_id, pos = event.data
+ client_id, location = event.data
server_player_id = self.client_players[client_id]
if server_player_id == ServerPlayer.spectator:
- print(f"Spectator cannot select {pos = }")
+ print("Spectator cannot control cursor")
await trio.lowlevel.checkpoint()
return
@@ -1045,13 +1075,27 @@ async def handle_cursor_location(
if player_id != self.state.current_turn:
print(
- "Player {player_id} (client ID {client_id}) cannot move cursor, not their turn.",
+ f"Player {player_id} (client ID {client_id}) cannot move cursor, not their turn.",
)
await trio.lowlevel.checkpoint()
return
- print(f"handle_cursor_location {client_id = } {pos = }")
- await trio.lowlevel.checkpoint()
+ # print(f"handle_cursor_location {client_id = } {location = }")
+
+ if self.internal_singleplayer_mode:
+ await trio.lowlevel.checkpoint()
+ return
+
+ async with trio.open_nursery() as nursery:
+ for other_client_id in self.client_players:
+ if other_client_id != client_id:
+ nursery.start_soon(
+ self.raise_event,
+ Event(
+ f"cursor_position->network[{other_client_id}]",
+ location,
+ ),
+ )
def __del__(self) -> None:
"""Debug print."""
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index ba625d4..b4f9fd5 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -573,15 +573,17 @@ async def move_destination_time(self, time_passed: float) -> None:
return
to_destination = self.to_destination()
- travel_distance = min(
- to_destination @ to_destination,
- movement.speed * time_passed,
- )
+ dest_magnitude = to_destination.magnitude()
+ travel_distance = movement.speed * time_passed
if travel_distance > 0:
- movement.move_heading_distance(travel_distance)
- # Fix imprecision
- self.update_heading()
+ if travel_distance > dest_magnitude:
+ sprite.location = self.destination
+ else:
+ # Fix imprecision
+ self.update_heading()
+ if travel_distance > 0:
+ movement.move_heading_distance(travel_distance)
await trio.lowlevel.checkpoint()
async def move_destination_time_ticks(
diff --git a/test-requirements.txt b/test-requirements.txt
index 904fa0f..4eece78 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -6,7 +6,7 @@ attrs==24.3.0
# trio
black==24.10.0 ; implementation_name == 'cpython'
# via -r test-requirements.in
-cffi==1.17.1 ; (os_name != 'nt' and platform_python_implementation != 'PyPy') or (implementation_name != 'pypy' and os_name == 'nt') or (implementation_name == 'pypy' and platform_python_implementation != 'PyPy')
+cffi==1.17.1 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_python_implementation != 'PyPy'
# via
# cryptography
# trio
@@ -14,7 +14,7 @@ click==8.1.7 ; implementation_name == 'cpython'
# via black
codespell==2.3.0
# via -r test-requirements.in
-colorama==0.4.6 ; (implementation_name != 'cpython' and sys_platform == 'win32') or (platform_system != 'Windows' and sys_platform == 'win32') or (implementation_name == 'cpython' and platform_system == 'Windows')
+colorama==0.4.6 ; (implementation_name == 'cpython' and platform_system == 'Windows') or sys_platform == 'win32'
# via
# click
# pytest
@@ -33,7 +33,7 @@ idna==3.10
# via trio
iniconfig==2.0.0
# via pytest
-libcomponent @ git+https://github.com/CoolCat467/LibComponent@b06eaca2f029a9fc019d68194ade260ccb00a841
+libcomponent @ git+https://github.com/CoolCat467/LibComponent@5c69fa04833560443dd3be414b5ce713793d5d42
# via -r test-requirements.in
mypy==1.13.0
# via -r test-requirements.in
@@ -61,7 +61,7 @@ platformdirs==4.3.6 ; implementation_name == 'cpython'
# via black
pluggy==1.5.0
# via pytest
-pycparser==2.22 ; (os_name != 'nt' and platform_python_implementation != 'PyPy') or (implementation_name != 'pypy' and os_name == 'nt') or (implementation_name == 'pypy' and platform_python_implementation != 'PyPy')
+pycparser==2.22 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_python_implementation != 'PyPy'
# via cffi
pygame==2.6.1
# via -r test-requirements.in
@@ -97,5 +97,5 @@ typing-extensions==4.12.2
# black
# libcomponent
# mypy
-uv==0.5.9
+uv==0.5.10
# via -r test-requirements.in
From c15bc1a743f7e8d873b9d7983aad2279b379626a Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Tue, 17 Dec 2024 23:41:42 -0600
Subject: [PATCH 23/67] Implement clicking table center and handle several edge
cases I hadn't considered previously
---
src/azul/client.py | 14 ++
src/azul/game.py | 74 +++++----
src/azul/network_shared.py | 1 +
src/azul/server.py | 315 +++++++++++++++++++++++++++----------
src/azul/state.py | 19 ++-
5 files changed, 300 insertions(+), 123 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index e2fe064..1813167 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -174,6 +174,7 @@ def __init__(self, name: str) -> None:
"factory_clicked->server[write]": sbe.factory_clicked,
"cursor_location->server[write]": sbe.cursor_location,
"pattern_row_clicked->server[write]": sbe.pattern_row_clicked,
+ "table_clicked->server[write]": sbe.table_clicked,
},
)
cbe = ClientBoundEvents
@@ -221,6 +222,7 @@ def bind_handlers(self) -> None:
"game_factory_clicked": self.write_game_factory_clicked,
"game_cursor_location_transmit": self.write_game_cursor_location_transmit,
"game_pattern_row_clicked": self.write_game_pattern_row_clicked,
+ "game_table_clicked": self.write_game_table_clicked,
},
)
@@ -492,6 +494,18 @@ async def write_game_pattern_row_clicked(
Event("pattern_row_clicked->server[write]", buffer),
)
+ async def write_game_table_clicked(
+ self,
+ event: Event[Tile],
+ ) -> None:
+ """Write table_clicked event to server."""
+ tile = event.data
+ buffer = Buffer()
+
+ buffer.write_value(StructFormat.UBYTE, tile)
+
+ await self.raise_event(Event("table_clicked->server[write]", buffer))
+
async def handle_network_stop(self, event: Event[None]) -> None:
"""Send EOF if connected and close socket."""
if self.not_connected:
diff --git a/src/azul/game.py b/src/azul/game.py
index 25ae555..e6fd2fa 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -678,37 +678,6 @@ async def handle_cursor_set_movement_mode(
self.unregister_handler_type("tick")
await trio.lowlevel.checkpoint()
- def get_held_count(self) -> int:
- """Return the number of held tiles."""
- return len(self.tiles)
-
- def is_holding(self) -> bool:
- """Return True if the mouse is dragging something."""
- return len(self.tiles) > 0
-
- def get_held_info(self) -> tuple[int, ...]:
- """Return tuple of currently held tiles."""
- return tuple(reversed(self.tiles))
-
- def drop(
- self,
- number: int | None = None,
- ) -> tuple[int, ...]:
- """Pop and return tiles the Cursor is carrying.
-
- If number is None, pops all tiles, otherwise only pops given count.
- """
- if number is None:
- tiles_copy = self.get_held_info()
- self.tiles.clear()
- self.update_image()
- return tiles_copy
- tiles: list[int] = []
- for _ in range(number):
- tiles.append(self.tiles.pop())
- self.update_image()
- return tuple(tiles)
-
class Grid(TileRenderer):
"""Grid object, used for boards and parts of other objects."""
@@ -1149,7 +1118,11 @@ async def handle_click(
return
index = int(point.y * 2 + point.x)
- tile_color = tuple(self.tiles.elements())[index]
+ tiles = tuple(self.tiles.elements())
+ if not tiles:
+ await trio.lowlevel.checkpoint()
+ return
+ tile_color = tiles[index]
if tile_color < 0:
# Do not send non-real tiles
@@ -1184,13 +1157,20 @@ def __init__(self) -> None:
self.update_image()
self.visible = True
+ self.add_component(sprite.DragClickEventComponent())
+
def __repr__(self) -> str:
"""Return representation of self."""
return f"{self.__class__.__name__}()"
def bind_handlers(self) -> None:
"""Register event handlers."""
- self.register_handler("game_table_data", self.update_board_data)
+ self.register_handlers(
+ {
+ "game_table_data": self.update_board_data,
+ "click": self.handle_click,
+ },
+ )
def iter_tiles(self) -> Generator[int, None, None]:
"""Yield tile colors."""
@@ -1246,6 +1226,34 @@ async def update_board_data(self, event: Event[Counter[int]]) -> None:
self.update_image()
await trio.lowlevel.checkpoint()
+ async def handle_click(
+ self,
+ event: Event[sprite.PygameMouseButtonEventData],
+ ) -> None:
+ """Handle click event."""
+ point = self.get_tile_point(event.data["pos"])
+ if point is None:
+ await trio.lowlevel.checkpoint()
+ return
+
+ index = int(point.y * 6 + point.x)
+ tile_color = tuple(self.iter_tiles())[index]
+
+ if tile_color < 0:
+ # Do not send non-real tiles
+ await trio.lowlevel.checkpoint()
+ return
+
+ # Transmit to server
+ # Needs level 2 to reach server client
+ await self.raise_event(
+ Event(
+ "game_table_clicked",
+ Tile(tile_color),
+ 2,
+ ),
+ )
+
class HaltState(AsyncState["AzulClient"]):
"""Halt state to set state to None so running becomes False."""
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index 79333b5..d9b7d1e 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -149,3 +149,4 @@ class ServerBoundEvents(IntEnum):
factory_clicked = auto()
cursor_location = auto()
pattern_row_clicked = auto()
+ table_clicked = auto()
diff --git a/src/azul/server.py b/src/azul/server.py
index e5f6b51..dc1f80b 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -115,6 +115,7 @@ def __init__(self, client_id: int) -> None:
sbe.factory_clicked: f"client[{self.client_id}]->factory_clicked",
sbe.cursor_location: f"client[{self.client_id}]->cursor_location",
sbe.pattern_row_clicked: f"client[{self.client_id}]->pattern_row_clicked",
+ sbe.table_clicked: f"client[{self.client_id}]->table_clicked",
},
)
@@ -127,6 +128,7 @@ def bind_handlers(self) -> None:
f"client[{self.client_id}]->factory_clicked": self.read_factory_clicked,
f"client[{self.client_id}]->cursor_location": self.read_cursor_location,
f"client[{self.client_id}]->pattern_row_clicked": self.read_pattern_row_clicked,
+ f"client[{self.client_id}]->table_clicked": self.read_table_clicked,
f"callback_ping->network[{self.client_id}]": self.handle_callback_ping,
"initial_config->network": self.write_factory_clicked,
f"playing_as->network[{self.client_id}]": self.write_playing_as,
@@ -204,6 +206,22 @@ async def read_pattern_row_clicked(self, event: Event[bytearray]) -> None:
),
)
+ async def read_table_clicked(self, event: Event[bytearray]) -> None:
+ """Read table_clicked event from client. Raise as `table_clicked->server`."""
+ buffer = Buffer(event.data)
+
+ tile_color = Tile(buffer.read_value(StructFormat.UBYTE))
+
+ await self.raise_event(
+ Event(
+ "table_clicked->server",
+ (
+ self.client_id,
+ tile_color,
+ ),
+ ),
+ )
+
async def handle_callback_ping(
self,
_: Event[None],
@@ -408,6 +426,7 @@ def bind_handlers(self) -> None:
"factory_clicked->server": self.handle_client_factory_clicked,
"pattern_row_clicked->server": self.handle_client_pattern_row_clicked,
"cursor_location->server": self.handle_cursor_location,
+ "table_clicked->server": self.handle_client_table_clicked,
},
)
@@ -557,51 +576,8 @@ async def start_server( # type: ignore[misc]
# Serve runs forever until canceled
nursery.start_soon(partial(self.serve, port, host, backlog=0))
- async def transmit_playing_as(self) -> None:
- """Transmit playing as."""
- async with trio.open_nursery() as nursery:
- for client_id, team in self.client_players.items():
- nursery.start_soon(
- self.raise_event,
- Event(f"playing_as->network[{client_id}]", team),
- )
-
- async def handle_server_start_new_game(self, event: Event[bool]) -> None:
- """Handle game start."""
- varient_play = event.data
- ## # Delete all pieces from last state (shouldn't be needed but still.)
- ## async with trio.open_nursery() as nursery:
- ## for piece_pos, _piece_type in self.state.get_pieces():
- ## nursery.start_soon(
- ## self.raise_event,
- ## Event("delete_piece->network", piece_pos),
- ## )
-
- # Choose which team plays first
- # Using non-cryptographically secure random because it doesn't matter
- self.new_game_init(varient_play)
-
- ## # Send create_piece events for all pieces
- ## async with trio.open_nursery() as nursery:
- ## for piece_pos, piece_type in self.state.get_pieces():
- ## nursery.start_soon(
- ## self.raise_event,
- ## Event("create_piece->network", (piece_pos, piece_type)),
- ## )
-
- # Raise initial config event with board size and initial turn.
- await self.raise_event(
- Event(
- "initial_config->network",
- (
- self.state.varient_play,
- len(self.state.player_data),
- len(self.state.factory_displays),
- self.state.current_turn,
- ),
- ),
- )
-
+ async def transmit_new_round_data(self) -> None:
+ """Transmit all player board data, factory data, and table center data."""
async with trio.open_nursery() as nursery:
# Transmit board data
for player_id, player_data in self.state.player_data.items():
@@ -638,9 +614,26 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
),
)
- await self.transmit_cursor_movement_mode()
-
- await self.transmit_playing_as()
+ async def transmit_pattern_line_data(self) -> None:
+ """Transmit all pattern line data for all players."""
+ async with trio.open_nursery() as nursery:
+ # Transmit pattern line data
+ for player_id, player_data in self.state.player_data.items():
+ for line_id, line_data in enumerate(player_data.lines):
+ nursery.start_soon(
+ self.raise_event,
+ Event(
+ "pattern_data->network",
+ (
+ player_id,
+ line_id,
+ (
+ max(0, int(line_data.color)),
+ line_data.count_,
+ ),
+ ),
+ ),
+ )
async def transmit_cursor_movement_mode(self) -> None:
"""Update current cursor movement mode for all clients."""
@@ -666,6 +659,57 @@ async def transmit_cursor_movement_mode(self) -> None:
),
)
+ async def transmit_playing_as(self) -> None:
+ """Transmit playing as."""
+ async with trio.open_nursery() as nursery:
+ for client_id, team in self.client_players.items():
+ nursery.start_soon(
+ self.raise_event,
+ Event(f"playing_as->network[{client_id}]", team),
+ )
+
+ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
+ """Handle game start."""
+ varient_play = event.data
+ ## # Delete all pieces from last state (shouldn't be needed but still.)
+ ## async with trio.open_nursery() as nursery:
+ ## for piece_pos, _piece_type in self.state.get_pieces():
+ ## nursery.start_soon(
+ ## self.raise_event,
+ ## Event("delete_piece->network", piece_pos),
+ ## )
+
+ # Choose which team plays first
+ # Using non-cryptographically secure random because it doesn't matter
+ self.new_game_init(varient_play)
+
+ ## # Send create_piece events for all pieces
+ ## async with trio.open_nursery() as nursery:
+ ## for piece_pos, piece_type in self.state.get_pieces():
+ ## nursery.start_soon(
+ ## self.raise_event,
+ ## Event("create_piece->network", (piece_pos, piece_type)),
+ ## )
+
+ # Raise initial config event with board size and initial turn.
+ await self.raise_event(
+ Event(
+ "initial_config->network",
+ (
+ self.state.varient_play,
+ len(self.state.player_data),
+ len(self.state.factory_displays),
+ self.state.current_turn,
+ ),
+ ),
+ )
+
+ await self.transmit_new_round_data()
+
+ await self.transmit_cursor_movement_mode()
+
+ await self.transmit_playing_as()
+
async def client_network_loop(
self,
client: ServerClient,
@@ -934,6 +978,69 @@ async def handle_client_factory_clicked(
),
)
+ async def handle_client_table_clicked(
+ self,
+ event: Event[tuple[int, Tile]],
+ ) -> None:
+ """Handle client clicked a table center tile."""
+ if not self.players_can_interact:
+ print("Players are not allowed to interact.")
+ await trio.lowlevel.checkpoint()
+ return
+
+ client_id, tile = event.data
+
+ server_player_id = self.client_players[client_id]
+
+ if server_player_id == ServerPlayer.spectator:
+ print(f"Spectator cannot select table center {tile}")
+ await trio.lowlevel.checkpoint()
+ return
+
+ player_id = int(server_player_id)
+ if server_player_id == ServerPlayer.singleplayer_all:
+ player_id = self.state.current_turn
+
+ if player_id != self.state.current_turn:
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select table center tile, not their turn.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if self.state.current_phase != Phase.factory_offer:
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select table center tile, not in factory offer phase.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if not self.state.can_cursor_select_center(
+ int(tile),
+ ):
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select table center tile, state says no.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ # Perform move
+ self.state = self.state.cursor_selects_table_center(int(tile))
+
+ # Send updates to client
+ await self.raise_event(
+ Event(
+ "cursor_data->network",
+ self.state.cursor_contents,
+ ),
+ )
+ await self.raise_event(
+ Event(
+ "table_data->network",
+ self.state.table_center,
+ ),
+ )
+
async def handle_client_pattern_row_clicked(
self,
event: Event[tuple[int, int, tuple[int, int]]],
@@ -979,13 +1086,36 @@ async def handle_client_pattern_row_clicked(
return
column, line_id = row_pos
- place_count = 5 - column
+ if line_id >= 5:
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} line {line_id} (invalid line id).",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+ if column >= 5:
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} column {column} (invalid column).",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ currently_placed = self.state.get_player_line_current_place_count(
+ line_id,
+ )
+
+ place_count = 5 - column - currently_placed
+
+ if self.state.is_cursor_empty():
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} when not holding tiles.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
color = self.state.get_cursor_holding_color()
- max_place = self.state.get_player_line_max_placable_count(line_id)
current_hold_count = self.state.cursor_contents[color]
- place_count = min(place_count, current_hold_count, max_place)
+ place_count = min(place_count, current_hold_count)
if not self.state.can_player_select_line(line_id, color, place_count):
print(
@@ -1001,47 +1131,43 @@ async def handle_client_pattern_row_clicked(
place_count,
)
- if self.state.current_turn != player_id:
- if not self.internal_singleplayer_mode:
- new_client_id = self.find_client_id_from_state_turn(
- self.state.current_turn,
- )
- assert new_client_id is not None
- await self.raise_event(
- Event(
- f"cursor_movement_mode->network[{client_id}]",
- False,
- ),
- )
- await self.raise_event(
- Event(
- f"cursor_movement_mode->network[{new_client_id}]",
- True,
- ),
- )
-
+ if (
+ self.state.current_turn != player_id
+ and not self.internal_singleplayer_mode
+ ):
+ new_client_id = self.find_client_id_from_state_turn(
+ self.state.current_turn,
+ )
+ assert new_client_id is not None
await self.raise_event(
Event(
- "current_turn_change->network",
- self.state.current_turn,
+ f"cursor_movement_mode->network[{client_id}]",
+ False,
+ ),
+ )
+ await self.raise_event(
+ Event(
+ f"cursor_movement_mode->network[{new_client_id}]",
+ True,
),
)
- raw_tile_color, tile_count = self.state.player_data[
- prev_player_turn
- ].lines[line_id]
- # Do not send blank colors, clamp to zero
- tile_color = max(0, int(raw_tile_color))
- await self.raise_event(
- Event(
- "pattern_data->network",
- (
- prev_player_turn,
- line_id,
- (tile_color, tile_count),
+ if self.state.current_phase != Phase.wall_tiling:
+ raw_tile_color, tile_count = self.state.player_data[
+ prev_player_turn
+ ].lines[line_id]
+ # Do not send blank colors, clamp to zero
+ tile_color = max(0, int(raw_tile_color))
+ await self.raise_event(
+ Event(
+ "pattern_data->network",
+ (
+ prev_player_turn,
+ line_id,
+ (tile_color, tile_count),
+ ),
),
- ),
- )
+ )
await self.raise_event(
Event(
@@ -1050,6 +1176,23 @@ async def handle_client_pattern_row_clicked(
),
)
+ if self.state.current_phase == Phase.end:
+ print("TODO: Handle end of game.")
+
+ if self.state.current_phase == Phase.wall_tiling:
+ if not self.state.varient_play:
+ self.state = self.state.apply_auto_wall_tiling()
+ await self.transmit_new_round_data()
+ await self.transmit_pattern_line_data()
+
+ if self.state.current_turn != player_id:
+ await self.raise_event(
+ Event(
+ "current_turn_change->network",
+ self.state.current_turn,
+ ),
+ )
+
async def handle_cursor_location(
self,
event: Event[tuple[int, tuple[int, int]]],
diff --git a/src/azul/state.py b/src/azul/state.py
index 6018c0f..99bfb6b 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -235,6 +235,11 @@ def get_line_max_count(line_id: int) -> int:
# Line id is keeping track of max count
return line_id + 1
+ def get_line_current_place_count(self, line_id: int) -> int:
+ """Return count of currently placed tiles for given line."""
+ assert self.line_id_valid(line_id)
+ return self.lines[line_id].count_
+
def get_line_max_placable_count(self, line_id: int) -> int:
"""Return max placable count for given line."""
assert self.line_id_valid(line_id)
@@ -833,6 +838,12 @@ def get_player_line_max_placable_count(self, line_id: int) -> int:
return player_data.get_line_max_placable_count(line_id)
+ def get_player_line_current_place_count(self, line_id: int) -> int:
+ """Return current place count for given line."""
+ player_data = self.player_data[self.current_turn]
+
+ return player_data.get_line_current_place_count(line_id)
+
def all_pullable_empty(self) -> bool:
"""Return if all pullable tile locations are empty, not counting cursor."""
if self.table_center.total():
@@ -857,13 +868,13 @@ def _factory_offer_maybe_next_turn(self) -> Self:
# Go to wall tiling phase
current_phase = Phase.wall_tiling
- new_state = self._replace(
+ ##if current_phase == Phase.wall_tiling and not self.varient_play:
+ ## return new_state.apply_auto_wall_tiling()
+ ##return new_state
+ return self._replace(
current_phase=current_phase,
current_turn=current_turn,
)
- if current_phase == Phase.wall_tiling and not self.varient_play:
- return new_state.apply_auto_wall_tiling()
- return new_state
def player_select_floor_line(self, color: int, place_count: int) -> Self:
"""Return new state after player adds tiles to floor line."""
From d2b89a0110452cdf7a3d92ddf9dc8337785ff256 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Wed, 18 Dec 2024 12:18:37 -0600
Subject: [PATCH 24/67] Render floor line
---
src/azul/game.py | 143 +++++++++++++++++++----------------------------
1 file changed, 58 insertions(+), 85 deletions(-)
diff --git a/src/azul/game.py b/src/azul/game.py
index e6fd2fa..5998000 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -407,13 +407,12 @@ def clear_image(
self.background,
)
- def blit_tile(
+ def get_tile_topleft(
self,
- tile_color: int,
tile_location: tuple[int, int],
offset: tuple[int, int] | None = None,
- ) -> None:
- """Blit the surface of a given tile object onto self.image at given tile location. It is assumed that all tile locations are xy tuples."""
+ ) -> tuple[int, int]:
+ """Return top left corner location of tile given its location and optional offset."""
tile_full = self.tile_size + self.tile_separation
position = Vector2.from_iter(tile_location) * tile_full
@@ -421,12 +420,35 @@ def blit_tile(
position += offset
position += (self.tile_separation, self.tile_separation)
+ return vec2_to_location(position)
+
+ def get_tile_rect(
+ self,
+ tile_location: tuple[int, int],
+ offset: tuple[int, int] | None = None,
+ ) -> Rect:
+ """Return Rect of area given tile exists in."""
+ topleft = self.get_tile_topleft(tile_location, offset)
+ return Rect(
+ topleft,
+ (self.tile_size, self.tile_size),
+ )
+
+ def blit_tile(
+ self,
+ tile_color: int,
+ tile_location: tuple[int, int],
+ offset: tuple[int, int] | None = None,
+ ) -> None:
+ """Blit the surface of a given tile object onto self.image at given tile location. It is assumed that all tile locations are xy tuples."""
+ position = self.get_tile_topleft(tile_location, offset)
+
surf = get_tile_image(tile_color, self.tile_size, self.greyshift)
assert self.image is not None
self.image.blit(
surf,
- vec2_to_location(position),
+ position,
)
def to_image_surface_location(
@@ -798,62 +820,6 @@ async def handle_game_board_data(
await trio.lowlevel.checkpoint()
-class Row(TileRenderer):
- """Represents one of the five rows each player has."""
-
- __slots__ = ("color", "count", "size")
- greyshift = GREYSHIFT
-
- def __init__(
- self,
- name: str,
- size: int,
- tile_separation: int | None = None,
- background: tuple[int, int, int] | None = None,
- ) -> None:
- """Initialize row."""
- super().__init__(
- name,
- tile_separation,
- background,
- )
-
- self.color = Tile.blank
- self.size = int(size)
- self.count = 0
-
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"{self.__class__.__name__}({self.size})"
-
- def update_image(self) -> None:
- """Update self.image."""
- self.clear_image((self.size, 1))
-
- for x in range(self.count, self.size):
- self.blit_tile(Tile.blank, (self.size - x, 0))
- for x in range(self.count):
- self.blit_tile(self.color, (self.size - x, 0))
- self.dirty = 1
-
- def get_placed(self) -> int:
- """Return the number of tiles in self that are not fake tiles, like grey ones."""
- return self.count
-
- def get_placeable(self) -> int:
- """Return the number of tiles permitted to be placed on self."""
- return self.size - self.get_placed()
-
- def is_full(self) -> bool:
- """Return True if this row is full."""
- return self.get_placeable() == 0
-
- def set_background(self, color: tuple[int, int, int] | None) -> None:
- """Set the background color for this row."""
- self.background = color
- self.update_image()
-
-
class PatternRows(TileRenderer):
"""Represents one of the five rows each player has."""
@@ -984,42 +950,45 @@ async def handle_game_pattern_data(
await trio.lowlevel.checkpoint()
-class FloorLine(Row):
+class FloorLine(TileRenderer):
"""Represents a player's floor line."""
- __slots__ = ("floor_line_id", "numbers", "text")
+ __slots__ = ("floor_line_id", "numbers", "size")
def __init__(self, floor_line_id: int) -> None:
"""Initialize floor line."""
- super().__init__(f"floor_line_{floor_line_id}", 7, background=ORANGE)
-
- # self.font = Font(FONT, round(self.tile_size*1.2), color=BLACK, cx=False, cy=False)
- self.text = objects.Text(
- "text object",
- pygame.font.Font(FONT, round(self.tile_size * 1.2)),
+ super().__init__(
+ f"floor_line_{floor_line_id}",
+ background=RED,
)
- self.text.color = BLACK
- self.numbers = [-255 for _ in range(self.size)]
+ self.size = 7
+
+ self.numbers = tuple(-1 for _ in range(self.size))
+ self.tiles: list[Tile] = []
+
+ self.update_image()
+ self.visible = True
def __repr__(self) -> str:
"""Return representation of self."""
- return f"{self.__class__.__name__}(<>)"
+ return f"{self.__class__.__name__}({self.floor_line_id})"
- def render(self, surface: pygame.surface.Surface) -> None:
+ def update_image(self) -> None:
"""Update self.image."""
- sx, sy = self.location
- w, h = self.rect.size
- tile_full = self.tile_separation + self.tile_size
- for x in range(self.size):
- xy = round(
- x * tile_full + self.tile_separation + sx - w / 2,
- ), round(
- self.tile_separation + sy - h / 2,
- )
- self.text.text = str(self.numbers[x])
- self.text.location = Vector2(*xy)
- # self.text.render(surface)
+ self.clear_image((self.size, 1))
+
+ font = pygame.font.Font(FONT, size=self.tile_size)
+
+ for x, tile in enumerate(self.tiles):
+ self.blit_tile(tile, (x, 0))
+ for x in range(len(self.tiles), self.size):
+ self.blit_tile(Tile.blank, (x, 0))
+ # Draw number on top
+ number_surf = font.render(str(self.numbers[x]), False, BLACK)
+ tile_topleft = self.get_tile_topleft((x, 0))
+ self.image.blit(number_surf, tile_topleft)
+ self.dirty = 1
class Factory(TileRenderer):
@@ -1861,6 +1830,10 @@ async def handle_game_initial_config(
pattern_rows.set_background(DARKGREEN)
self.group_add(pattern_rows)
+ floor_line = FloorLine(index)
+ floor_line.rect.topleft = pattern_rows.rect.bottomleft
+ self.group_add(floor_line)
+
degrees += each
async def check_conditions(self) -> str | None:
From d212ff263b388b9f9cae766df26a25e0d23c77ed Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Wed, 18 Dec 2024 12:37:23 -0600
Subject: [PATCH 25/67] Use numbers length instead of keeping track of size
---
src/azul/game.py | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/azul/game.py b/src/azul/game.py
index 5998000..f830428 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -953,7 +953,7 @@ async def handle_game_pattern_data(
class FloorLine(TileRenderer):
"""Represents a player's floor line."""
- __slots__ = ("floor_line_id", "numbers", "size")
+ __slots__ = ("floor_line_id", "numbers")
def __init__(self, floor_line_id: int) -> None:
"""Initialize floor line."""
@@ -962,9 +962,7 @@ def __init__(self, floor_line_id: int) -> None:
background=RED,
)
- self.size = 7
-
- self.numbers = tuple(-1 for _ in range(self.size))
+ self.numbers = tuple(-1 for _ in range(7))
self.tiles: list[Tile] = []
self.update_image()
@@ -976,13 +974,13 @@ def __repr__(self) -> str:
def update_image(self) -> None:
"""Update self.image."""
- self.clear_image((self.size, 1))
+ self.clear_image((len(self.numbers), 1))
font = pygame.font.Font(FONT, size=self.tile_size)
for x, tile in enumerate(self.tiles):
self.blit_tile(tile, (x, 0))
- for x in range(len(self.tiles), self.size):
+ for x in range(len(self.tiles), len(self.numbers)):
self.blit_tile(Tile.blank, (x, 0))
# Draw number on top
number_surf = font.render(str(self.numbers[x]), False, BLACK)
From 19d7938dd77a02bd75182cfcf624df5329d4805a Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sun, 22 Dec 2024 13:28:19 -0600
Subject: [PATCH 26/67] WIP implement being able to click floor line
---
src/azul/client.py | 23 +++++++++++++---
src/azul/game.py | 55 +++++++++++++++++++++++++++++++++++---
src/azul/mr_floppy_test.py | 45 ++++++++++++++++++++++++-------
src/azul/network_shared.py | 1 +
src/azul/server.py | 10 +++++--
test-requirements.txt | 10 +++----
6 files changed, 119 insertions(+), 25 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index 1813167..d4f9c3b 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -175,6 +175,7 @@ def __init__(self, name: str) -> None:
"cursor_location->server[write]": sbe.cursor_location,
"pattern_row_clicked->server[write]": sbe.pattern_row_clicked,
"table_clicked->server[write]": sbe.table_clicked,
+ "floor_clicked->server[write]": sbe.floor_clicked,
},
)
cbe = ClientBoundEvents
@@ -223,6 +224,7 @@ def bind_handlers(self) -> None:
"game_cursor_location_transmit": self.write_game_cursor_location_transmit,
"game_pattern_row_clicked": self.write_game_pattern_row_clicked,
"game_table_clicked": self.write_game_table_clicked,
+ "game_floor_clicked": self.write_game_floor_clicked,
},
)
@@ -464,7 +466,7 @@ async def write_game_factory_clicked(
buffer.write_value(StructFormat.UBYTE, factory_id)
buffer.write_value(StructFormat.UBYTE, tile)
- await self.raise_event(Event("factory_clicked->server[write]", buffer))
+ await self.write_event(Event("factory_clicked->server[write]", buffer))
async def write_game_cursor_location_transmit(
self,
@@ -476,7 +478,7 @@ async def write_game_cursor_location_transmit(
x, y = map(int, (scaled_location * 0xFFF).floored())
buffer = encode_cursor_location((x, y))
- await self.raise_event(Event("cursor_location->server[write]", buffer))
+ await self.write_event(Event("cursor_location->server[write]", buffer))
async def write_game_pattern_row_clicked(
self,
@@ -490,7 +492,7 @@ async def write_game_pattern_row_clicked(
buffer.write_value(StructFormat.UBYTE, int(location.x))
buffer.write_value(StructFormat.UBYTE, int(location.y))
- await self.raise_event(
+ await self.write_event(
Event("pattern_row_clicked->server[write]", buffer),
)
@@ -504,7 +506,20 @@ async def write_game_table_clicked(
buffer.write_value(StructFormat.UBYTE, tile)
- await self.raise_event(Event("table_clicked->server[write]", buffer))
+ await self.write_event(Event("table_clicked->server[write]", buffer))
+
+ async def write_game_floor_clicked(
+ self,
+ event: Event[tuple[int, int]],
+ ) -> None:
+ """Write floor_clicked event to server."""
+ floor_line_id, location_x = event.data
+ buffer = Buffer()
+
+ buffer.write_value(StructFormat.UBYTE, floor_line_id)
+ buffer.write_value(StructFormat.UBYTE, location_x)
+
+ await self.write_event(Event("floor_clicked->server[write]", buffer))
async def handle_network_stop(self, event: Event[None]) -> None:
"""Send EOF if connected and close socket."""
diff --git a/src/azul/game.py b/src/azul/game.py
index f830428..c366249 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -591,6 +591,7 @@ def bind_handlers(self) -> None:
"cursor_reached_destination": self.handle_cursor_reached_destination,
"game_cursor_set_destination": self.handle_cursor_set_destination,
"game_cursor_set_movement_mode": self.handle_cursor_set_movement_mode,
+ "client_disconnected": self.handle_network_stop,
},
)
@@ -700,6 +701,15 @@ async def handle_cursor_set_movement_mode(
self.unregister_handler_type("tick")
await trio.lowlevel.checkpoint()
+ async def handle_network_stop(
+ self,
+ event: Event[None],
+ ) -> None:
+ """Unregister tick event handler."""
+ print(f"[azul.game.Cursor] Got {event = }")
+ self.unregister_handler_type("tick")
+ await trio.lowlevel.checkpoint()
+
class Grid(TileRenderer):
"""Grid object, used for boards and parts of other objects."""
@@ -962,6 +972,10 @@ def __init__(self, floor_line_id: int) -> None:
background=RED,
)
+ self.add_component(sprite.DragClickEventComponent())
+
+ self.floor_line_id = floor_line_id
+
self.numbers = tuple(-1 for _ in range(7))
self.tiles: list[Tile] = []
@@ -988,6 +1002,36 @@ def update_image(self) -> None:
self.image.blit(number_surf, tile_topleft)
self.dirty = 1
+ def bind_handlers(self) -> None:
+ """Register event handlers."""
+ self.register_handlers(
+ {
+ "click": self.handle_click,
+ },
+ )
+
+ async def handle_click(
+ self,
+ event: Event[sprite.PygameMouseButtonEventData],
+ ) -> None:
+ """Handle click event."""
+ point = self.get_tile_point(event.data["pos"])
+ if point is None:
+ await trio.lowlevel.checkpoint()
+ return
+
+ # Transmit to server
+ await self.raise_event(
+ Event(
+ "game_floor_clicked",
+ (
+ self.floor_line_id,
+ int(point.floored().x),
+ ),
+ 2,
+ ),
+ )
+
class Factory(TileRenderer):
"""Represents a Factory."""
@@ -1868,7 +1912,7 @@ async def handle_game_over(self, event: Event[int]) -> None:
async def handle_client_disconnected(self, event: Event[str]) -> None:
"""Handle client disconnected error."""
error = event.data
- print(f"handle_client_disconnected {error = }")
+ print(f"[azul.game.PlayState] handle_client_disconnected {error = }")
self.exit_data = (1, f"Client Disconnected$${error}", False)
@@ -1878,11 +1922,11 @@ async def do_actions(self) -> None:
if self.exit_data is None:
return
- exit_status, message, handled = self.exit_data
+ exit_status, raw_message, handled = self.exit_data
if handled:
return
- self.exit_data = (exit_status, message, True)
+ self.exit_data = (exit_status, raw_message, True)
font = pygame.font.Font(
FONT,
@@ -1891,7 +1935,9 @@ async def do_actions(self) -> None:
error_message = ""
if exit_status == 1:
- message, error_message = message.split("$$")
+ message, error_message = raw_message.split("$$", 1)
+ else:
+ message = raw_message
if not self.manager.component_exists("continue_button"):
continue_button = KwargButton(
@@ -1913,6 +1959,7 @@ async def do_actions(self) -> None:
if exit_status == 1:
if not self.manager.component_exists("error_text"):
error_text = objects.OutlinedText("error_text", font)
+ error_text.text = ""
else:
error_text = self.manager.get_component("error_text")
error_text.visible = True
diff --git a/src/azul/mr_floppy_test.py b/src/azul/mr_floppy_test.py
index 5275dda..878df8c 100644
--- a/src/azul/mr_floppy_test.py
+++ b/src/azul/mr_floppy_test.py
@@ -47,7 +47,7 @@
FONT = FONT_FOLDER / "RuneScape-UF-Regular.ttf"
-class AzulClient(sprite.GroupProcessor, AsyncStateMachine):
+class GameClient(sprite.GroupProcessor, AsyncStateMachine):
"""Gear Runner and Layered Dirty Sprite group handler."""
def __init__(self) -> None:
@@ -78,7 +78,7 @@ async def raise_event(self, event: Event[Any]) -> None:
await manager.raise_event(event)
-class AzulState(AsyncState[AzulClient]):
+class AzulState(AsyncState[GameClient]):
"""Azul Client Asynchronous base class."""
__slots__ = ("id", "manager")
@@ -120,7 +120,7 @@ def bind_handlers(self) -> None:
self.register_handlers(
{
"click": self.click,
- "drag": self.drag,
+ # "drag": self.drag,
"PygameMouseButtonDown": self.mouse_down,
"tick": self.move_towards_dest,
"init": self.cache_outline,
@@ -132,12 +132,16 @@ async def test(self, event: Event[object]) -> None:
"""Print out event data."""
print(f"{event = }")
+ await trio.lowlevel.checkpoint()
+
async def cache_outline(self, _: Event[None]) -> None:
"""Precalculate outlined images."""
image: sprite.ImageComponent = self.get_component("image")
outline: sprite.OutlineComponent = image.get_component("outline")
outline.precalculate_all_outlined(self.outline)
+ await trio.lowlevel.checkpoint()
+
async def update_selected(self) -> None:
"""Update selected."""
image: sprite.ImageComponent = self.get_component("image")
@@ -150,15 +154,19 @@ async def update_selected(self) -> None:
movement: sprite.MovementComponent = self.get_component("movement")
movement.speed = 0
+ await trio.lowlevel.checkpoint()
+
async def click(
self,
event: Event[sprite.PygameMouseButtonEventData],
) -> None:
"""Toggle selected."""
- if event.data["button"] == 1:
- self.selected = not self.selected
+ if event.data["button"] != 1:
+ await trio.lowlevel.checkpoint()
+ return
+ self.selected = not self.selected
- await self.update_selected()
+ await self.update_selected()
async def drag(self, event: Event[None]) -> None:
"""Drag sprite."""
@@ -168,12 +176,15 @@ async def drag(self, event: Event[None]) -> None:
movement: sprite.MovementComponent = self.get_component("movement")
movement.speed = 0
+ await trio.lowlevel.checkpoint()
+
async def mouse_down(
self,
event: Event[sprite.PygameMouseButtonEventData],
) -> None:
"""Target click pos if selected."""
if not self.selected:
+ await trio.lowlevel.checkpoint()
return
if event.data["button"] == 1:
movement: sprite.MovementComponent = self.get_component("movement")
@@ -181,6 +192,8 @@ async def mouse_down(
target: sprite.TargetingComponent = self.get_component("targeting")
target.destination = Vector2.from_iter(event.data["pos"])
+ await trio.lowlevel.checkpoint()
+
async def move_towards_dest(
self,
event: Event[sprite.TickEventData],
@@ -199,12 +212,14 @@ def __init__(self) -> None:
"""Initialize mr floppy sprite."""
super().__init__("MrFloppy")
+ image_component = sprite.ImageComponent()
+ image_component.add_component(sprite.AnimationComponent())
self.add_components(
(
sprite.MovementComponent(),
sprite.TargetingComponent(),
ClickDestinationComponent(),
- sprite.ImageComponent(),
+ image_component,
sprite.DragClickEventComponent(),
),
)
@@ -257,11 +272,14 @@ def controller(
async def drag(self, event: Event[sprite.DragEvent]) -> None:
"""Move by relative from drag."""
- if not event.data.buttons[1]:
+ if not event.data.buttons.get(1):
+ await trio.lowlevel.checkpoint()
return
self.location += event.data.rel
self.dirty = 1
+ await trio.lowlevel.checkpoint()
+
class FPSCounter(objects.Text):
"""FPS counter."""
@@ -273,11 +291,16 @@ def __init__(self) -> None:
font = pygame.font.Font(FONT, 28)
super().__init__("fps", font)
+ self.text = "FPS: ???"
+ self.visible = True
+
async def on_tick(self, event: Event[sprite.TickEventData]) -> None:
"""Update text."""
# self.text = f'FPS: {event.data["fps"]:.2f}'
self.text = f"FPS: {event.data.fps:.0f}"
+ await trio.lowlevel.checkpoint()
+
async def update_loc(
self,
event: Event[dict[str, tuple[int, int]]],
@@ -316,7 +339,7 @@ async def entry_actions(self) -> None:
"""Create group and add mr floppy."""
self.id = self.machine.new_group("test")
floppy = MrFloppy()
- print(floppy)
+ print(f"{floppy = }")
self.group_add(floppy)
self.group_add(FPSCounter())
@@ -327,6 +350,8 @@ async def exit_actions(self) -> None:
self.machine.remove_group(self.id)
self.manager.unbind_components()
+ await trio.lowlevel.checkpoint()
+
def save_crash_img() -> None:
"""Save the last frame before the game crashed."""
@@ -355,7 +380,7 @@ async def async_run() -> None:
pygame.key.set_repeat(1000, 30)
screen.fill((0xFF, 0xFF, 0xFF))
- client = AzulClient()
+ client = GameClient()
background = pygame.image.load(
path.join("data", "background.png"),
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index d9b7d1e..76010f1 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -150,3 +150,4 @@ class ServerBoundEvents(IntEnum):
cursor_location = auto()
pattern_row_clicked = auto()
table_clicked = auto()
+ floor_clicked = auto()
diff --git a/src/azul/server.py b/src/azul/server.py
index dc1f80b..79a60f7 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -748,14 +748,20 @@ async def client_network_loop(
except network.NetworkEOFError:
print(f"{client.name} EOF")
break
+ except RuntimeError as exc:
+ traceback.print_exception(exc)
+ print(f"{client.name} Bad packet")
+ break
except (
trio.BrokenResourceError,
trio.ClosedResourceError,
- RuntimeError,
- ):
+ ) as exc:
+ traceback.print_exception(exc)
+ print(f"{client.name} Socket connection issue")
break
except Exception as exc:
traceback.print_exception(exc)
+ print(f"{client.name} Unhandled exception")
break
if event is not None:
# if controls_lobby:
diff --git a/test-requirements.txt b/test-requirements.txt
index 4eece78..71fa0ea 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -10,7 +10,7 @@ cffi==1.17.1 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_p
# via
# cryptography
# trio
-click==8.1.7 ; implementation_name == 'cpython'
+click==8.1.8 ; implementation_name == 'cpython'
# via black
codespell==2.3.0
# via -r test-requirements.in
@@ -33,9 +33,9 @@ idna==3.10
# via trio
iniconfig==2.0.0
# via pytest
-libcomponent @ git+https://github.com/CoolCat467/LibComponent@5c69fa04833560443dd3be414b5ce713793d5d42
+libcomponent @ git+https://github.com/CoolCat467/LibComponent@91362869af19943520430fbed9edc38fc473ac9e
# via -r test-requirements.in
-mypy==1.13.0
+mypy==1.14.0
# via -r test-requirements.in
mypy-extensions==1.0.0
# via
@@ -74,7 +74,7 @@ pytest-cov==6.0.0
# via -r test-requirements.in
pytest-trio==0.8.0
# via -r test-requirements.in
-ruff==0.8.3
+ruff==0.8.4
# via -r test-requirements.in
sniffio==1.3.1
# via trio
@@ -97,5 +97,5 @@ typing-extensions==4.12.2
# black
# libcomponent
# mypy
-uv==0.5.10
+uv==0.5.11
# via -r test-requirements.in
From acb31b9287222ad5c6cd2fb94e5e53d1ea32987b Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sun, 22 Dec 2024 13:31:20 -0600
Subject: [PATCH 27/67] Add `crate-ci/typos` pre-commit hook and associated
spelling fixes
---
.pre-commit-config.yaml | 6 ++++++
src/azul/client.py | 4 ++--
src/azul/game.py | 2 +-
src/azul/server.py | 22 +++++++++++-----------
src/azul/sprite.py | 6 +++---
src/azul/state.py | 34 +++++++++++++++++-----------------
src/azul/vector.py | 2 +-
tests/test_async_clock.py | 2 +-
8 files changed, 42 insertions(+), 36 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ff6904d..d965914 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -36,6 +36,12 @@ repos:
rev: v2.3.0
hooks:
- id: codespell
+ additional_dependencies:
+ - tomli
+ - repo: https://github.com/crate-ci/typos
+ rev: v1.28.4
+ hooks:
+ - id: typos
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v0.9.2
hooks:
diff --git a/src/azul/client.py b/src/azul/client.py
index d4f9c3b..7911144 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -345,7 +345,7 @@ async def read_initial_config(self, event: Event[bytearray]) -> None:
"""Read initial_config event from server."""
buffer = Buffer(event.data)
- varient_play: u8 = buffer.read_value(StructFormat.BOOL)
+ variant_play: u8 = buffer.read_value(StructFormat.BOOL)
player_count: u8 = buffer.read_value(StructFormat.UBYTE)
factory_count: u8 = buffer.read_value(StructFormat.UBYTE)
current_turn: u8 = buffer.read_value(StructFormat.UBYTE)
@@ -354,7 +354,7 @@ async def read_initial_config(self, event: Event[bytearray]) -> None:
Event(
"game_initial_config",
(
- varient_play,
+ variant_play,
player_count,
factory_count,
current_turn,
diff --git a/src/azul/game.py b/src/azul/game.py
index c366249..6081afa 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -1830,7 +1830,7 @@ async def handle_game_initial_config(
event: Event[tuple[bool, int, int, int]],
) -> None:
"""Handle `game_initial_config` event."""
- varient_play, player_count, factory_count, self.current_turn = (
+ variant_play, player_count, factory_count, self.current_turn = (
event.data
)
diff --git a/src/azul/server.py b/src/azul/server.py
index 79a60f7..ca18422 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -234,11 +234,11 @@ async def write_factory_clicked(
event: Event[tuple[bool, int, int, int]],
) -> None:
"""Read initial config event and reraise as server[write]->initial_config."""
- varient_play, player_count, factory_count, current_turn = event.data
+ variant_play, player_count, factory_count, current_turn = event.data
buffer = Buffer()
- buffer.write_value(StructFormat.BOOL, varient_play)
+ buffer.write_value(StructFormat.BOOL, variant_play)
buffer.write_value(StructFormat.UBYTE, player_count)
buffer.write_value(StructFormat.UBYTE, factory_count)
buffer.write_value(StructFormat.UBYTE, current_turn)
@@ -531,14 +531,14 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]:
players[client_id] = ServerPlayer.spectator
return players
- def new_game_init(self, varient_play: bool = False) -> None:
+ def new_game_init(self, variant_play: bool = False) -> None:
"""Start new game."""
print("server new_game_init")
self.client_players.clear()
self.state = State.new_game(
max(2, min(4, self.client_count)),
- varient_play,
+ variant_play,
)
# Why keep track of another object just to know client ID numbers
@@ -670,7 +670,7 @@ async def transmit_playing_as(self) -> None:
async def handle_server_start_new_game(self, event: Event[bool]) -> None:
"""Handle game start."""
- varient_play = event.data
+ variant_play = event.data
## # Delete all pieces from last state (shouldn't be needed but still.)
## async with trio.open_nursery() as nursery:
## for piece_pos, _piece_type in self.state.get_pieces():
@@ -681,7 +681,7 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
# Choose which team plays first
# Using non-cryptographically secure random because it doesn't matter
- self.new_game_init(varient_play)
+ self.new_game_init(variant_play)
## # Send create_piece events for all pieces
## async with trio.open_nursery() as nursery:
@@ -696,7 +696,7 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
Event(
"initial_config->network",
(
- self.state.varient_play,
+ self.state.variant_play,
len(self.state.player_data),
len(self.state.factory_displays),
self.state.current_turn,
@@ -796,7 +796,7 @@ async def send_spectator_join_packets(
Event(
"initial_config->network",
(
- self.state.varient_play,
+ self.state.variant_play,
len(self.state.player_data),
len(self.state.factory_displays),
self.state.current_turn,
@@ -853,9 +853,9 @@ async def handler(self, stream: trio.SocketStream) -> None:
with self.temporary_component(client):
if can_start and not game_active: # and is_zee_capitan:
print("[azul.server] game start trigger.")
- varient_play = False
+ variant_play = False
await self.raise_event(
- Event("server_send_game_start", varient_play),
+ Event("server_send_game_start", variant_play),
)
try:
await self.client_network_loop(client, is_zee_capitan)
@@ -1186,7 +1186,7 @@ async def handle_client_pattern_row_clicked(
print("TODO: Handle end of game.")
if self.state.current_phase == Phase.wall_tiling:
- if not self.state.varient_play:
+ if not self.state.variant_play:
self.state = self.state.apply_auto_wall_tiling()
await self.transmit_new_round_data()
await self.transmit_pattern_line_data()
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index b4f9fd5..f753ddb 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -325,7 +325,7 @@ def set_color(self, color: Color | None) -> None:
assert manager.set_surface is not None
manager.set_image(manager.set_surface)
- def get_outline_discriptor(self, identifier: str | int) -> str:
+ def get_outline_descriptor(self, identifier: str | int) -> str:
"""Return outlined identifier for given original identifier."""
color = "_".join(map(str, self.__color))
return f"{identifier}{self.mod}{color}_{self.size}"
@@ -334,7 +334,7 @@ def save_outline(self, identifier: str | int) -> None:
"""Save outlined version of given identifier image."""
manager = cast(ImageComponent, self.manager)
- outlined = self.get_outline_discriptor(identifier)
+ outlined = self.get_outline_descriptor(identifier)
if manager.image_exists(outlined):
return
@@ -367,7 +367,7 @@ def save_outline(self, identifier: str | int) -> None:
def get_outline(self, identifier: str | int) -> str:
"""Return saved outline effect identifier."""
self.save_outline(identifier)
- return self.get_outline_discriptor(identifier)
+ return self.get_outline_descriptor(identifier)
def precalculate_outline(
self,
diff --git a/src/azul/state.py b/src/azul/state.py
index 99bfb6b..09f0176 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -183,8 +183,8 @@ def floor_fill_tile_excess(
return excess
-class UnplacableTileError(Exception):
- """Unplacable Tile Exception."""
+class UnplayableTileError(Exception):
+ """Unplayable Tile Exception."""
__slots__ = ("y",)
@@ -202,11 +202,11 @@ class PlayerData(NamedTuple):
floor: Counter[int]
@classmethod
- def new(cls, varient_play: bool = False) -> Self:
+ def new(cls, variant_play: bool = False) -> Self:
"""Return new player data instance."""
wall = full((5, 5), Tile.blank, int8)
- if not varient_play:
+ if not variant_play:
for y in range(5):
for x in range(5):
color = -((5 - y + x) % len(REAL_TILES) + 1)
@@ -483,7 +483,7 @@ def perform_end_of_game_scoring(self) -> Self:
def get_manual_wall_tile_location(self) -> tuple[int, list[int]] | None:
"""Return tuple of row and placable columns for wall tiling, or None if done.
- Raises UnplacableTileError if no valid placement locations.
+ Raises UnplayableTileError if no valid placement locations.
"""
for y, line in enumerate(self.lines):
if line.color == Tile.blank:
@@ -499,11 +499,11 @@ def get_manual_wall_tile_location(self) -> tuple[int, list[int]] | None:
continue
valid_x.append(x)
if not valid_x:
- raise UnplacableTileError(y)
+ raise UnplayableTileError(y)
return (y, valid_x)
return None
- def handle_unplacable_wall_tiling(
+ def handle_unplayable_wall_tiling(
self,
y: int,
) -> tuple[Self, Counter[int]]:
@@ -646,7 +646,7 @@ class SelectableDestinationTiles(NamedTuple):
class State(NamedTuple):
"""Represents state of an azul game."""
- varient_play: bool
+ variant_play: bool
current_phase: Phase
bag: Counter[int]
box_lid: Counter[int]
@@ -660,7 +660,7 @@ class State(NamedTuple):
def blank(cls) -> Self:
"""Return new blank state."""
return cls(
- varient_play=False,
+ variant_play=False,
current_phase=Phase.end,
bag=Counter(),
box_lid=Counter(),
@@ -672,7 +672,7 @@ def blank(cls) -> Self:
)
@classmethod
- def new_game(cls, player_count: int, varient_play: bool = False) -> Self:
+ def new_game(cls, player_count: int, variant_play: bool = False) -> Self:
"""Return state of a new game."""
factory_count = player_count * 2 + 1
bag = generate_bag_contents()
@@ -685,7 +685,7 @@ def new_game(cls, player_count: int, varient_play: bool = False) -> Self:
factory_displays[x] = tiles
return cls(
- varient_play=varient_play,
+ variant_play=variant_play,
current_phase=Phase.factory_offer,
bag=bag,
box_lid=Counter(),
@@ -694,7 +694,7 @@ def new_game(cls, player_count: int, varient_play: bool = False) -> Self:
cursor_contents=Counter(),
current_turn=0,
player_data={
- x: PlayerData.new(varient_play) for x in range(player_count)
+ x: PlayerData.new(variant_play) for x in range(player_count)
},
)
@@ -868,7 +868,7 @@ def _factory_offer_maybe_next_turn(self) -> Self:
# Go to wall tiling phase
current_phase = Phase.wall_tiling
- ##if current_phase == Phase.wall_tiling and not self.varient_play:
+ ##if current_phase == Phase.wall_tiling and not self.variant_play:
## return new_state.apply_auto_wall_tiling()
##return new_state
return self._replace(
@@ -1030,7 +1030,7 @@ def apply_destination_select_action_factory_offer(
def apply_auto_wall_tiling(self) -> Self:
"""Return new state after performing automatic wall tiling."""
assert self.current_phase == Phase.wall_tiling
- assert not self.varient_play
+ assert not self.variant_play
box_lid = self.box_lid.copy()
new_players = player_data_deepcopy(self.player_data)
@@ -1191,12 +1191,12 @@ def get_manual_wall_tiling_locations_for_player(
try:
return current_player_data.get_manual_wall_tile_location()
- except UnplacableTileError as unplacable_exc:
+ except UnplayableTileError as unplayable_exc:
# kind of hacky, but it works
- y_position = unplacable_exc.y
+ y_position = unplayable_exc.y
new_player_data, for_box_lid = (
- current_player_data.handle_unplacable_wall_tiling(y_position)
+ current_player_data.handle_unplayable_wall_tiling(y_position)
)
box_lid = self.box_lid.copy()
diff --git a/src/azul/vector.py b/src/azul/vector.py
index 57d50a0..f9aa830 100644
--- a/src/azul/vector.py
+++ b/src/azul/vector.py
@@ -41,7 +41,7 @@
# As a forward to the madness below, we are doing something incredibly sneeky.
# We have BaseVector, which we want to have all of the shared functionality
# of all Vector subclasses. We also want each Vector class to be a NamedTuple
-# so we can let Python handle storing data in the most efficiant way and
+# so we can let Python handle storing data in the most efficient way and
# make Vectors immutable.
#
# Problem is, we can't have Vector classes be
diff --git a/tests/test_async_clock.py b/tests/test_async_clock.py
index 7c97954..4297787 100644
--- a/tests/test_async_clock.py
+++ b/tests/test_async_clock.py
@@ -28,7 +28,7 @@ def test_get_time(clock: Clock) -> None:
@pytest.mark.trio
-async def test_tick_elasped(clock: Clock) -> None:
+async def test_tick_elapsed(clock: Clock) -> None:
time_passed = await clock.tick()
assert time_passed >= 0
From f24f2f29fe24fefa0a32c1b06cd2a996a1c4cc6b Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sun, 22 Dec 2024 13:52:46 -0600
Subject: [PATCH 28/67] Stop writing events after client disconnects
---
src/azul/client.py | 27 +++++++++++++++++++++++++++
src/azul/game.py | 7 ++++---
2 files changed, 31 insertions(+), 3 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index 7911144..420f967 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -244,10 +244,37 @@ async def raise_disconnect(self, message: str) -> None:
f"{self.__class__.__name__}: Manager does not exist, not raising disconnect event.",
)
return
+ # self.unregister_all_network_write_events()
await self.raise_event(Event("client_disconnected", message))
await self.close()
assert self.not_connected
+ async def write_event(
+ self,
+ event: Event[bytes | bytearray | memoryview],
+ ) -> None:
+ """Send event to network if running, otherwise does nothing.
+
+ Raises:
+ RuntimeError: if unregistered packet id received from network
+ trio.BusyResourceError: if another task is already executing a
+ :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or
+ :meth:`HalfCloseableStream.send_eof` on this stream.
+ trio.BrokenResourceError: if something has gone wrong, and the stream
+ is broken.
+ trio.ClosedResourceError: if you previously closed this stream
+ object, or if another task closes this stream object while
+ :meth:`send_all` is running.
+
+ """
+ if not self.running:
+ await trio.lowlevel.checkpoint()
+ print(
+ f"[azul.client.write_event] Skipping writing {event.name!r}, not running.",
+ )
+ return
+ await super().write_event(event)
+
async def handle_read_event(self) -> None:
"""Raise events from server.
diff --git a/src/azul/game.py b/src/azul/game.py
index 6081afa..b0cf55c 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -539,6 +539,7 @@ class Cursor(TileRenderer):
- cursor_reached_destination
- cursor_set_destination
- cursor_set_movement_mode
+ - client_disconnected
Sometimes registered:
- PygameMouseMotion
@@ -591,7 +592,7 @@ def bind_handlers(self) -> None:
"cursor_reached_destination": self.handle_cursor_reached_destination,
"game_cursor_set_destination": self.handle_cursor_set_destination,
"game_cursor_set_movement_mode": self.handle_cursor_set_movement_mode,
- "client_disconnected": self.handle_network_stop,
+ "client_disconnected": self.handle_client_disconnected,
},
)
@@ -701,12 +702,12 @@ async def handle_cursor_set_movement_mode(
self.unregister_handler_type("tick")
await trio.lowlevel.checkpoint()
- async def handle_network_stop(
+ async def handle_client_disconnected(
self,
event: Event[None],
) -> None:
"""Unregister tick event handler."""
- print(f"[azul.game.Cursor] Got {event = }")
+ print("[azul.game.Cursor] Got client disconnect, unregistering tick")
self.unregister_handler_type("tick")
await trio.lowlevel.checkpoint()
From 87276051e89b7d72378d90879cc0b771c274dbc6 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sun, 22 Dec 2024 17:01:19 -0600
Subject: [PATCH 29/67] Implement server-side handling for floor line
---
src/azul/client.py | 28 +++++-
src/azul/game.py | 43 +++++++--
src/azul/network_shared.py | 9 +-
src/azul/server.py | 183 ++++++++++++++++++++++++++++++++++---
src/azul/state.py | 2 +-
5 files changed, 236 insertions(+), 29 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index 420f967..88d1be8 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -194,6 +194,7 @@ def __init__(self, name: str) -> None:
cbe.cursor_movement_mode: "server->cursor_movement_mode",
cbe.current_turn_change: "server->current_turn_change",
cbe.cursor_position: "server->cursor_position",
+ cbe.floor_data: "server->floor_data",
},
)
@@ -218,6 +219,7 @@ def bind_handlers(self) -> None:
"server->cursor_movement_mode": self.read_cursor_movement_mode,
"server->current_turn_change": self.read_current_turn_change,
"server->cursor_position": self.read_cursor_position,
+ "server->floor_data": self.read_floor_data,
"client_connect": self.handle_client_connect,
"network_stop": self.handle_network_stop,
"game_factory_clicked": self.write_game_factory_clicked,
@@ -225,16 +227,19 @@ def bind_handlers(self) -> None:
"game_pattern_row_clicked": self.write_game_pattern_row_clicked,
"game_table_clicked": self.write_game_table_clicked,
"game_floor_clicked": self.write_game_floor_clicked,
+ # "callback_ping": self.print_callback_ping,
},
)
- async def print_callback_ping(self, event: Event[bytearray]) -> None:
+ async def print_callback_ping(self, event: Event[int]) -> None:
"""Print received `callback_ping` event from server.
This event is used as a sort of keepalive heartbeat, because
it stops the connection from timing out.
"""
- print(f"print_callback_ping {event = }")
+ difference = event.data
+ print(f"[azul.client] print_callback_ping {difference * 1e-06:.03f}ms")
+ await trio.lowlevel.checkpoint()
async def raise_disconnect(self, message: str) -> None:
"""Raise client_disconnected event with given message."""
@@ -298,6 +303,7 @@ async def handle_read_event(self) -> None:
if self.not_connected:
await self.raise_disconnect("Not connected to server.")
return
+ # event: Event[bytearray] | None = None
try:
# print("handle_read_event start")
event = await self.read_event()
@@ -307,6 +313,7 @@ async def handle_read_event(self) -> None:
print(f"[{self.name}] Socket closed from another task.")
return
except network.NetworkTimeoutError as exc:
+ # print("[azul.client] Network timeout")
if self.running:
self.running = False
print(f"[{self.name}] NetworkTimeoutError")
@@ -332,6 +339,8 @@ async def handle_read_event(self) -> None:
)
return
+ ## print(f'[azul.client] handle_read_event {event}')
+
await self.raise_event(event)
async def handle_client_connect(
@@ -376,6 +385,9 @@ async def read_initial_config(self, event: Event[bytearray]) -> None:
player_count: u8 = buffer.read_value(StructFormat.UBYTE)
factory_count: u8 = buffer.read_value(StructFormat.UBYTE)
current_turn: u8 = buffer.read_value(StructFormat.UBYTE)
+ floor_line_size: u8 = buffer.read_value(StructFormat.UBYTE)
+
+ floor_line_data = decode_int8_array(buffer, (floor_line_size, 1))
await self.raise_event(
Event(
@@ -385,6 +397,7 @@ async def read_initial_config(self, event: Event[bytearray]) -> None:
player_count,
factory_count,
current_turn,
+ floor_line_data,
),
),
)
@@ -482,6 +495,17 @@ async def read_cursor_position(self, event: Event[bytearray]) -> None:
Event("game_cursor_set_destination", unit_location),
)
+ async def read_floor_data(self, event: Event[bytearray]) -> None:
+ """Read floor_data event from server, reraise as `game_floor_data`."""
+ buffer = Buffer(event.data)
+
+ floor_id: u8 = buffer.read_value(StructFormat.UBYTE)
+ floor_line = decode_numeric_uint8_counter(buffer)
+
+ await self.raise_event(
+ Event("game_floor_data", (floor_id, floor_line)),
+ )
+
async def write_game_factory_clicked(
self,
event: Event[tuple[int, Tile]],
diff --git a/src/azul/game.py b/src/azul/game.py
index b0cf55c..ece5b38 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -85,6 +85,7 @@
Sequence,
)
+ from mypy_extensions import u8
from numpy.typing import NDArray
from typing_extensions import TypeVarTuple
@@ -966,7 +967,7 @@ class FloorLine(TileRenderer):
__slots__ = ("floor_line_id", "numbers")
- def __init__(self, floor_line_id: int) -> None:
+ def __init__(self, floor_line_id: int, numbers: NDArray[int8]) -> None:
"""Initialize floor line."""
super().__init__(
f"floor_line_{floor_line_id}",
@@ -977,8 +978,8 @@ def __init__(self, floor_line_id: int) -> None:
self.floor_line_id = floor_line_id
- self.numbers = tuple(-1 for _ in range(7))
- self.tiles: list[Tile] = []
+ self.numbers = tuple(numbers.flat)
+ self.tiles: Counter[Tile] = Counter()
self.update_image()
self.visible = True
@@ -993,9 +994,9 @@ def update_image(self) -> None:
font = pygame.font.Font(FONT, size=self.tile_size)
- for x, tile in enumerate(self.tiles):
+ for x, tile in enumerate(sorted(self.tiles.elements(), reverse=True)):
self.blit_tile(tile, (x, 0))
- for x in range(len(self.tiles), len(self.numbers)):
+ for x in range(self.tiles.total(), len(self.numbers)):
self.blit_tile(Tile.blank, (x, 0))
# Draw number on top
number_surf = font.render(str(self.numbers[x]), False, BLACK)
@@ -1007,10 +1008,28 @@ def bind_handlers(self) -> None:
"""Register event handlers."""
self.register_handlers(
{
+ "game_floor_data": self.handle_game_floor_data,
"click": self.handle_click,
},
)
+ async def handle_game_floor_data(
+ self,
+ event: Event[tuple[int, Counter[u8]]],
+ ) -> None:
+ """Handle game_floor_data event."""
+ line_id, floor_data = event.data
+
+ if line_id != self.floor_line_id:
+ await trio.lowlevel.checkpoint()
+ return
+
+ self.tiles.clear()
+ self.tiles.update({Tile(k): v for k, v in floor_data.items()})
+ self.update_image()
+
+ await trio.lowlevel.checkpoint()
+
async def handle_click(
self,
event: Event[sprite.PygameMouseButtonEventData],
@@ -1828,12 +1847,16 @@ async def entry_actions(self) -> None:
async def handle_game_initial_config(
self,
- event: Event[tuple[bool, int, int, int]],
+ event: Event[tuple[bool, int, int, int, NDArray[int8]]],
) -> None:
"""Handle `game_initial_config` event."""
- variant_play, player_count, factory_count, self.current_turn = (
- event.data
- )
+ (
+ variant_play,
+ player_count,
+ factory_count,
+ self.current_turn,
+ floor_line_data,
+ ) = event.data
center = Vector2.from_iter(SCREEN_SIZE) // 2
@@ -1873,7 +1896,7 @@ async def handle_game_initial_config(
pattern_rows.set_background(DARKGREEN)
self.group_add(pattern_rows)
- floor_line = FloorLine(index)
+ floor_line = FloorLine(index, floor_line_data)
floor_line.rect.topleft = pattern_rows.rect.bottomleft
self.group_add(floor_line)
diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py
index 76010f1..65b2ba1 100644
--- a/src/azul/network_shared.py
+++ b/src/azul/network_shared.py
@@ -45,7 +45,7 @@
Pos: TypeAlias = tuple[u8, u8]
-def encode_tile_count(tile_color: int, tile_count: int) -> Buffer:
+def encode_tile_count(tile_color: u8, tile_count: u8) -> Buffer:
"""Return buffer from tile color and count."""
buffer = Buffer()
@@ -55,7 +55,7 @@ def encode_tile_count(tile_color: int, tile_count: int) -> Buffer:
return buffer
-def decode_tile_count(buffer: Buffer) -> tuple[int, int]:
+def decode_tile_count(buffer: Buffer) -> tuple[u8, u8]:
"""Read and return tile color and count from buffer."""
tile_color = buffer.read_value(StructFormat.UBYTE)
tile_count = buffer.read_value(StructFormat.UBYTE)
@@ -76,9 +76,9 @@ def encode_numeric_uint8_counter(counter: Counter[int]) -> Buffer:
return buffer
-def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[int]:
+def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[u8]:
"""Read and return uint8 counter from buffer."""
- data: dict[int, int] = {}
+ data: dict[u8, u8] = {}
pair_count = buffer.read_value(StructFormat.UBYTE)
for _ in range(pair_count):
@@ -140,6 +140,7 @@ class ClientBoundEvents(IntEnum):
cursor_movement_mode = auto()
current_turn_change = auto()
cursor_position = auto()
+ floor_data = auto()
class ServerBoundEvents(IntEnum):
diff --git a/src/azul/server.py b/src/azul/server.py
index ca18422..214ad94 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -46,6 +46,7 @@
ServerClientNetworkEventComponent,
find_ip,
)
+from numpy import array, int8
from azul.network_shared import (
ADVERTISEMENT_IP,
@@ -59,13 +60,12 @@
encode_numeric_uint8_counter,
encode_tile_count,
)
-from azul.state import Phase, State, Tile
+from azul.state import FLOOR_LINE_DATA, Phase, State, Tile
if TYPE_CHECKING:
from collections import Counter
from collections.abc import Awaitable, Callable
- from numpy import int8
from numpy.typing import NDArray
@@ -106,6 +106,7 @@ def __init__(self, client_id: int) -> None:
"server[write]->cursor_movement_mode": cbe.cursor_movement_mode,
"server[write]->current_turn_change": cbe.current_turn_change,
"server[write]->cursor_position": cbe.cursor_position,
+ "server[write]->floor_data": cbe.floor_data,
},
)
sbe = ServerBoundEvents
@@ -116,6 +117,7 @@ def __init__(self, client_id: int) -> None:
sbe.cursor_location: f"client[{self.client_id}]->cursor_location",
sbe.pattern_row_clicked: f"client[{self.client_id}]->pattern_row_clicked",
sbe.table_clicked: f"client[{self.client_id}]->table_clicked",
+ sbe.floor_clicked: f"client[{self.client_id}]->floor_clicked",
},
)
@@ -129,8 +131,9 @@ def bind_handlers(self) -> None:
f"client[{self.client_id}]->cursor_location": self.read_cursor_location,
f"client[{self.client_id}]->pattern_row_clicked": self.read_pattern_row_clicked,
f"client[{self.client_id}]->table_clicked": self.read_table_clicked,
+ f"client[{self.client_id}]->floor_clicked": self.read_floor_clicked,
f"callback_ping->network[{self.client_id}]": self.handle_callback_ping,
- "initial_config->network": self.write_factory_clicked,
+ "initial_config->network": self.write_initial_config,
f"playing_as->network[{self.client_id}]": self.write_playing_as,
"game_over->network": self.write_game_over,
"board_data->network": self.write_board_data,
@@ -141,6 +144,7 @@ def bind_handlers(self) -> None:
f"cursor_position->network[{self.client_id}]": self.write_cursor_position,
"current_turn_change->network": self.write_current_turn_change,
"pattern_data->network": self.write_pattern_data,
+ "floor_data->network": self.write_floor_data,
},
)
@@ -222,6 +226,24 @@ async def read_table_clicked(self, event: Event[bytearray]) -> None:
),
)
+ async def read_floor_clicked(self, event: Event[bytearray]) -> None:
+ """Read floor_clicked event from client. Raise as `floor_clicked->server`."""
+ buffer = Buffer(event.data)
+
+ floor_line_id = buffer.read_value(StructFormat.UBYTE)
+ floor_line_pos_x = buffer.read_value(StructFormat.UBYTE)
+
+ await self.raise_event(
+ Event(
+ "floor_clicked->server",
+ (
+ self.client_id,
+ floor_line_id,
+ floor_line_pos_x,
+ ),
+ ),
+ )
+
async def handle_callback_ping(
self,
_: Event[None],
@@ -229,12 +251,14 @@ async def handle_callback_ping(
"""Reraise as server[write]->callback_ping."""
await self.write_callback_ping()
- async def write_factory_clicked(
+ async def write_initial_config(
self,
- event: Event[tuple[bool, int, int, int]],
+ event: Event[tuple[bool, int, int, int, tuple[int, ...]]],
) -> None:
"""Read initial config event and reraise as server[write]->initial_config."""
- variant_play, player_count, factory_count, current_turn = event.data
+ variant_play, player_count, factory_count, current_turn, floor_data = (
+ event.data
+ )
buffer = Buffer()
@@ -242,6 +266,8 @@ async def write_factory_clicked(
buffer.write_value(StructFormat.UBYTE, player_count)
buffer.write_value(StructFormat.UBYTE, factory_count)
buffer.write_value(StructFormat.UBYTE, current_turn)
+ buffer.write_value(StructFormat.UBYTE, len(floor_data))
+ buffer.extend(encode_int8_array(array(floor_data, dtype=int8)))
await self.write_event(Event("server[write]->initial_config", buffer))
@@ -370,6 +396,21 @@ async def write_pattern_data(
Event("server[write]->pattern_data", buffer),
)
+ async def write_floor_data(
+ self,
+ event: Event[tuple[int, Counter[int]]],
+ ) -> None:
+ """Reraise as server[write]->floor_data."""
+ floor_id, floor_line = event.data
+
+ buffer = Buffer()
+ buffer.write_value(StructFormat.UBYTE, floor_id)
+ buffer.extend(encode_numeric_uint8_counter(floor_line))
+
+ await self.write_event(
+ Event("server[write]->floor_data", buffer),
+ )
+
class ServerPlayer(IntEnum):
"""Server Player enum."""
@@ -427,6 +468,7 @@ def bind_handlers(self) -> None:
"pattern_row_clicked->server": self.handle_client_pattern_row_clicked,
"cursor_location->server": self.handle_cursor_location,
"table_clicked->server": self.handle_client_table_clicked,
+ "floor_clicked->server": self.handle_client_floor_clicked,
},
)
@@ -579,8 +621,8 @@ async def start_server( # type: ignore[misc]
async def transmit_new_round_data(self) -> None:
"""Transmit all player board data, factory data, and table center data."""
async with trio.open_nursery() as nursery:
- # Transmit board data
for player_id, player_data in self.state.player_data.items():
+ # Transmit board data
nursery.start_soon(
self.raise_event,
Event(
@@ -591,6 +633,17 @@ async def transmit_new_round_data(self) -> None:
),
),
)
+ # Transmit floor line data
+ nursery.start_soon(
+ self.raise_event,
+ Event(
+ "floor_data->network",
+ (
+ player_id,
+ player_data.floor,
+ ),
+ ),
+ )
# Transmit factory data
for (
factory_id,
@@ -606,6 +659,7 @@ async def transmit_new_round_data(self) -> None:
),
),
)
+
# Transmit table center data
await self.raise_event(
Event(
@@ -700,6 +754,7 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None:
len(self.state.player_data),
len(self.state.factory_displays),
self.state.current_turn,
+ FLOOR_LINE_DATA,
),
),
)
@@ -729,6 +784,7 @@ async def client_network_loop(
while not self.can_start() and not client.not_connected:
try:
await client.write_callback_ping()
+ await trio.sleep(1.5)
except (
trio.BrokenResourceError,
trio.ClosedResourceError,
@@ -739,8 +795,8 @@ async def client_network_loop(
while not client.not_connected:
event: Event[bytearray] | None = None
try:
- await client.write_callback_ping()
- with trio.move_on_after(2):
+ # await client.write_callback_ping()
+ with trio.move_on_after(1.5):
event = await client.read_event()
except network.NetworkTimeoutError:
print(f"{client.name} Timeout")
@@ -768,6 +824,7 @@ async def client_network_loop(
# print(f"{client.name} client_network_loop tick")
# print(f"{client.name} {event = }")
await client.raise_event(event)
+ await client.write_callback_ping()
def can_start(self) -> bool:
"""Return if game can start."""
@@ -800,6 +857,7 @@ async def send_spectator_join_packets(
len(self.state.player_data),
len(self.state.factory_displays),
self.state.current_turn,
+ FLOOR_LINE_DATA,
),
),
)
@@ -1158,6 +1216,16 @@ async def handle_client_pattern_row_clicked(
),
)
+ await self.raise_event(
+ Event(
+ "floor_data->network",
+ (
+ player_id,
+ self.state.player_data[player_id].floor,
+ ),
+ ),
+ )
+
if self.state.current_phase != Phase.wall_tiling:
raw_tile_color, tile_count = self.state.player_data[
prev_player_turn
@@ -1182,15 +1250,15 @@ async def handle_client_pattern_row_clicked(
),
)
- if self.state.current_phase == Phase.end:
- print("TODO: Handle end of game.")
-
if self.state.current_phase == Phase.wall_tiling:
if not self.state.variant_play:
self.state = self.state.apply_auto_wall_tiling()
await self.transmit_new_round_data()
await self.transmit_pattern_line_data()
+ if self.state.current_phase == Phase.end:
+ print("TODO: Handle end of game.")
+
if self.state.current_turn != player_id:
await self.raise_event(
Event(
@@ -1246,6 +1314,97 @@ async def handle_cursor_location(
),
)
+ async def handle_client_floor_clicked(
+ self,
+ event: Event[tuple[int, int, int]],
+ ) -> None:
+ """Handle client clicking floor line."""
+ if not self.players_can_interact:
+ print("Players are not allowed to interact.")
+ await trio.lowlevel.checkpoint()
+ return
+
+ client_id, floor_line_id, location_x = event.data
+
+ server_player_id = self.client_players[client_id]
+
+ if server_player_id == ServerPlayer.spectator:
+ print("Spectator cannot select floor line")
+ await trio.lowlevel.checkpoint()
+ return
+
+ player_id = int(server_player_id)
+ if server_player_id == ServerPlayer.singleplayer_all:
+ player_id = self.state.current_turn
+
+ if player_id != self.state.current_turn:
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select floor line, not their turn.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if self.state.current_phase != Phase.factory_offer:
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select floor line, not in factory offer phase.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if player_id != floor_line_id:
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select floor line {floor_line_id} that does not belong to them.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ if self.state.is_cursor_empty():
+ print(
+ f"Player {player_id} (client ID {client_id}) cannot select floor line when not holding tiles.",
+ )
+ await trio.lowlevel.checkpoint()
+ return
+
+ color = self.state.get_cursor_holding_color()
+
+ place_count = min(location_x + 1, self.state.cursor_contents[color])
+
+ self.state = self.state.player_select_floor_line(color, place_count)
+
+ await self.raise_event(
+ Event(
+ "cursor_data->network",
+ self.state.cursor_contents,
+ ),
+ )
+
+ await self.raise_event(
+ Event(
+ "floor_data->network",
+ (
+ player_id,
+ self.state.player_data[player_id].floor,
+ ),
+ ),
+ )
+
+ if self.state.current_phase == Phase.wall_tiling:
+ if not self.state.variant_play:
+ self.state = self.state.apply_auto_wall_tiling()
+ await self.transmit_new_round_data()
+ await self.transmit_pattern_line_data()
+
+ if self.state.current_phase == Phase.end:
+ print("TODO: Handle end of game.")
+
+ if self.state.current_turn != player_id:
+ await self.raise_event(
+ Event(
+ "current_turn_change->network",
+ self.state.current_turn,
+ ),
+ )
+
def __del__(self) -> None:
"""Debug print."""
print(f"del {self.__class__.__name__}")
diff --git a/src/azul/state.py b/src/azul/state.py
index 09f0176..c801135 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -569,7 +569,7 @@ def manual_wall_tiling_action(
)
def finish_manual_wall_tiling(self) -> tuple[Self, Counter[int], bool]:
- """Return new player data and tiles for box lid after performing automatic wall tiling."""
+ """Return new player data and tiles for box lid after performing manual wall tiling."""
for_box_lid: Counter[int] = Counter()
score = self.score
From 4cc0d65b9dabb90c047e4eeb1723df187e2648ba Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Mon, 23 Dec 2024 16:01:15 -0600
Subject: [PATCH 30/67] Update pre-commit hooks
---
.pre-commit-config.yaml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d965914..b644634 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -23,7 +23,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.8.3
+ rev: v0.8.4
hooks:
- id: ruff
types: [file]
@@ -43,14 +43,14 @@ repos:
hooks:
- id: typos
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v0.9.2
+ rev: v0.10.0
hooks:
- id: zizmor
- repo: local
hooks:
- id: project-requirements
name: regenerate requirements.in
- language: system
- entry: python tools/project_requirements.py
+ language: python
+ entry: tools/project_requirements.py
pass_filenames: false
files: ^(test-requirements.in)|(pyproject.toml)$
From bc8cdca31716ca31b4d02d485c471aea55a995b4 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Mon, 30 Dec 2024 12:50:45 -0600
Subject: [PATCH 31/67] Update dependencies & project
---
.github/workflows/autodeps.yml | 4 +-
.github/workflows/ci.yml | 174 +++++++++++++++++----------------
.pre-commit-config.yaml | 2 +-
pyproject.toml | 4 +-
test-requirements.in | 6 +-
test-requirements.txt | 14 +--
6 files changed, 103 insertions(+), 101 deletions(-)
diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml
index 5918fd9..7a08a2b 100644
--- a/.github/workflows/autodeps.yml
+++ b/.github/workflows/autodeps.yml
@@ -39,8 +39,8 @@ jobs:
run: python -m pip install -r test-requirements.txt
# apply newer versions' formatting
- - name: Black
- run: black src/azul
+ - name: Pre-commit updates
+ run: pre-commit run -a
- name: uv
run: |
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b6bbbe7..24e4a67 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,52 +11,52 @@ concurrency:
cancel-in-progress: true
jobs:
-## Windows:
-## name: 'Windows (${{ matrix.python }}, ${{ matrix.arch }}${{ matrix.extra_name }})'
-## timeout-minutes: 20
-## runs-on: 'windows-latest'
-## strategy:
-## fail-fast: false
-## matrix:
-## python: ['3.10', '3.11', '3.12']
-## arch: ['x86', 'x64']
-## continue-on-error: >-
-## ${{
-## (
-## endsWith(matrix.python, '-dev')
-## || endsWith(matrix.python, '-nightly')
-## )
-## && true
-## || false
-## }}
-## steps:
-## - name: Checkout
-## uses: actions/checkout@v4
-## - name: Setup python
-## uses: actions/setup-python@v5
-## with:
-## # This allows the matrix to specify just the major.minor version while still
-## # expanding it to get the latest patch version including alpha releases.
-## # This avoids the need to update for each new alpha, beta, release candidate,
-## # and then finally an actual release version. actions/setup-python doesn't
-## # support this for PyPy presently so we get no help there.
-## #
-## # 'CPython' -> '3.9.0-alpha - 3.9.X'
-## # 'PyPy' -> 'pypy-3.9'
-## python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }}
-## architecture: '${{ matrix.arch }}'
-## cache: pip
-## cache-dependency-path: test-requirements.txt
-## - name: Run tests
-## run: ./ci.sh
-## shell: bash
+ Windows:
+ name: 'Windows (${{ matrix.python }}, ${{ matrix.arch }}${{ matrix.extra_name }})'
+ timeout-minutes: 20
+ runs-on: 'windows-latest'
+ strategy:
+ fail-fast: false
+ matrix:
+ python: ['3.10', '3.11', '3.12']
+ arch: ['x86', 'x64']
+ continue-on-error: >-
+ ${{
+ (
+ endsWith(matrix.python, '-dev')
+ || endsWith(matrix.python, '-nightly')
+ )
+ && true
+ || false
+ }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - name: Setup python
+ uses: actions/setup-python@v5
+ with:
+ # This allows the matrix to specify just the major.minor version while still
+ # expanding it to get the latest patch version including alpha releases.
+ # This avoids the need to update for each new alpha, beta, release candidate,
+ # and then finally an actual release version. actions/setup-python doesn't
+ # support this for PyPy presently so we get no help there.
+ #
+ # 'CPython' -> '3.9.0-alpha - 3.9.X'
+ # 'PyPy' -> 'pypy-3.9'
+ python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }}
+ architecture: '${{ matrix.arch }}'
+ cache: pip
+ cache-dependency-path: test-requirements.txt
+ - name: Run tests
+ run: ./ci.sh
+ shell: bash
Ubuntu:
name: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})'
timeout-minutes: 10
runs-on: 'ubuntu-latest'
- # Only run for PRs or pushes to main
- if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
strategy:
fail-fast: false
matrix:
@@ -98,49 +98,51 @@ jobs:
env:
CHECK_FORMATTING: '${{ matrix.check_formatting }}'
-## macOS:
-## name: 'macOS (${{ matrix.python }})'
-## timeout-minutes: 15
-## runs-on: 'macos-latest'
-## strategy:
-## fail-fast: false
-## matrix:
-## python: ['3.10', '3.11', '3.12']
-## continue-on-error: >-
-## ${{
-## (
-## endsWith(matrix.python, '-dev')
-## || endsWith(matrix.python, '-nightly')
-## )
-## && true
-## || false
-## }}
-## steps:
-## - name: Checkout
-## uses: actions/checkout@v4
-## - name: Setup python
-## uses: actions/setup-python@v5
-## with:
-## python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }}
-## cache: pip
-## cache-dependency-path: test-requirements.txt
-## - name: Run tests
-## run: ./ci.sh
+ macOS:
+ name: 'macOS (${{ matrix.python }})'
+ timeout-minutes: 15
+ runs-on: 'macos-latest'
+ strategy:
+ fail-fast: false
+ matrix:
+ python: ['3.10', '3.11', '3.12']
+ continue-on-error: >-
+ ${{
+ (
+ endsWith(matrix.python, '-dev')
+ || endsWith(matrix.python, '-nightly')
+ )
+ && true
+ || false
+ }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - name: Setup python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }}
+ cache: pip
+ cache-dependency-path: test-requirements.txt
+ - name: Run tests
+ run: ./ci.sh
+
+ # https://github.com/marketplace/actions/alls-green#why
+ check: # This job does nothing and is only used for the branch protection
+
+ if: always()
+
+ needs:
+ - Windows
+ - Ubuntu
+ - macOS
-## # https://github.com/marketplace/actions/alls-green#why
-## check: # This job does nothing and is only used for the branch protection
-##
-## if: always()
-##
-## needs:
-## - Windows
-## - Ubuntu
-## - macOS
-##
-## runs-on: ubuntu-latest
-##
-## steps:
-## - name: Decide whether the needed jobs succeeded or failed
-## uses: re-actors/alls-green@release/v1
-## with:
-## jobs: ${{ toJSON(needs) }}
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Decide whether the needed jobs succeeded or failed
+ uses: re-actors/alls-green@release/v1
+ with:
+ jobs: ${{ toJSON(needs) }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b644634..c8d04bc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ ci:
autofix_prs: true
autoupdate_schedule: quarterly
submodules: false
- skip: [badgie, project-requirements]
+ skip: [badgie]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
diff --git a/pyproject.toml b/pyproject.toml
index 4012fde..3ddaa43 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,11 +36,11 @@ keywords = [
"ai", "multi-player", "azul", "ai-support", "networked-game"
]
dependencies = [
- "libcomponent @ git+https://github.com/CoolCat467/LibComponent",
+ "libcomponent~=0.0.1",
"pygame~=2.6.0",
"typing_extensions>=4.12.2",
"mypy_extensions>=1.0.0",
- "trio~=0.27.0",
+ "trio~=0.28.0",
"exceptiongroup; python_version < '3.11'",
"numpy~=2.1.3",
]
diff --git a/test-requirements.in b/test-requirements.in
index a9dc141..1e6d53e 100644
--- a/test-requirements.in
+++ b/test-requirements.in
@@ -6,7 +6,7 @@ pytest-cov
# Tools
black; implementation_name == "cpython"
-mypy >= 1.13.0 # Would use mypy[faster-cache], but orjson has build issues on pypy
+mypy >= 1.14.0 # Would use mypy[faster-cache], but orjson has build issues on pypy
orjson; implementation_name == "cpython"
ruff >= 0.6.6
uv >= 0.2.24
@@ -19,10 +19,10 @@ typing-extensions
# Azul's own dependencies
#
exceptiongroup; python_version < '3.11'
-libcomponent @ git+https://github.com/CoolCat467/LibComponent
+libcomponent~=0.0.1
mypy_extensions>=1.0.0
numpy~=2.1.3
pygame~=2.6.0
-trio~=0.27.0
+trio~=0.28.0
typing_extensions>=4.12.2
#
diff --git a/test-requirements.txt b/test-requirements.txt
index 71fa0ea..dd75553 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -14,11 +14,11 @@ click==8.1.8 ; implementation_name == 'cpython'
# via black
codespell==2.3.0
# via -r test-requirements.in
-colorama==0.4.6 ; (implementation_name == 'cpython' and platform_system == 'Windows') or sys_platform == 'win32'
+colorama==0.4.6 ; sys_platform == 'win32'
# via
# click
# pytest
-coverage==7.6.9
+coverage==7.6.10
# via
# -r test-requirements.in
# pytest-cov
@@ -33,9 +33,9 @@ idna==3.10
# via trio
iniconfig==2.0.0
# via pytest
-libcomponent @ git+https://github.com/CoolCat467/LibComponent@91362869af19943520430fbed9edc38fc473ac9e
+libcomponent==0.0.1
# via -r test-requirements.in
-mypy==1.14.0
+mypy==1.14.1
# via -r test-requirements.in
mypy-extensions==1.0.0
# via
@@ -45,7 +45,7 @@ mypy-extensions==1.0.0
# mypy
numpy==2.1.3
# via -r test-requirements.in
-orjson==3.10.12 ; implementation_name == 'cpython'
+orjson==3.10.13 ; implementation_name == 'cpython'
# via -r test-requirements.in
outcome==1.3.0.post0
# via
@@ -86,7 +86,7 @@ tomli==2.2.1 ; python_full_version <= '3.11'
# coverage
# mypy
# pytest
-trio==0.27.0
+trio==0.28.0
# via
# -r test-requirements.in
# libcomponent
@@ -97,5 +97,5 @@ typing-extensions==4.12.2
# black
# libcomponent
# mypy
-uv==0.5.11
+uv==0.5.13
# via -r test-requirements.in
From 30aa3420c21bc2984817a68fc3bb7b1f0aa3680a Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 4 Jan 2025 04:44:18 -0600
Subject: [PATCH 32/67] Work on getting machine client working
---
computer_players/MiniMax_AI.py | 83 ++++-----
computer_players/machine_client.py | 276 +++++++++++++++++++++++------
computer_players/minimax.py | 11 +-
pyproject.toml | 2 +-
4 files changed, 278 insertions(+), 94 deletions(-)
diff --git a/computer_players/MiniMax_AI.py b/computer_players/MiniMax_AI.py
index bbc0865..3da2d9c 100755
--- a/computer_players/MiniMax_AI.py
+++ b/computer_players/MiniMax_AI.py
@@ -13,7 +13,7 @@
from typing import TYPE_CHECKING, TypeAlias, TypeVar
-##from machine_client import RemoteState, run_clients_in_local_servers_sync
+from machine_client import RemoteState, run_clients_in_local_servers_sync
from minimax import Minimax, MinimaxResult, Player
from azul.state import (
@@ -37,13 +37,13 @@
# 1 = True = AI (Us) = MAX = 1, 3
-class AzulMinimax(Minimax[State, Action]):
+class AzulMinimax(Minimax[tuple[State, int], Action]):
"""Minimax Algorithm for Checkers."""
__slots__ = ()
@staticmethod
- def value(state: State) -> int | float:
+ def value(state: tuple[State, int]) -> int | float:
"""Return value of given game state."""
# Real
real_state, max_player = state
@@ -68,13 +68,13 @@ def value(state: State) -> int | float:
return (max_ - min_) / (abs(max_) + abs(min_) + 1)
@staticmethod
- def terminal(state: State) -> bool:
+ def terminal(state: tuple[State, int]) -> bool:
"""Return if game state is terminal."""
real_state, _max_player = state
return real_state.current_phase == Phase.end
@staticmethod
- def player(state: State) -> Player:
+ def player(state: tuple[State, int]) -> Player:
"""Return Player enum from current state's turn."""
real_state, max_player = state
return (
@@ -82,62 +82,65 @@ def player(state: State) -> Player:
)
@staticmethod
- def actions(state: State) -> Iterable[Action]:
+ def actions(state: tuple[State, int]) -> Iterable[Action]:
"""Return all actions that are able to be performed for the current player in the given state."""
real_state, _max_player = state
return tuple(real_state.yield_actions())
## print(f'{len(actions) = }')
@staticmethod
- def result(state: State, action: Action) -> State:
+ def result(state: tuple[State, int], action: Action) -> tuple[State, int]:
"""Return new state after performing given action on given current state."""
real_state, max_player = state
return (real_state.preform_action(action), max_player)
@classmethod
- def adaptive_depth_minimax(cls, state: State) -> MinimaxResult[Action]:
+ def adaptive_depth_minimax(
+ cls,
+ state: tuple[State, int],
+ ) -> MinimaxResult[Action]:
"""Adaptive depth minimax."""
# TODO
depth = 1
return cls.alphabeta(state, depth)
-##class MinimaxPlayer(RemoteState):
-## """Minimax Player."""
-##
-## __slots__ = ()
-##
-## async def preform_turn(self) -> Action:
-## """Perform turn."""
-## print("preform_turn")
-## ##value, action = CheckersMinimax.adaptive_depth_minimax(
-## ## self.state, 4, 5
-## ##)
-## ##value, action = CheckersMinimax.minimax(self.state, 4)
-## value, action = CheckersMinimax.alphabeta(self.state, 4)
-## if action is None:
-## raise ValueError("action is None")
-## print(f"{value = }")
-## return action
+class MinimaxPlayer(RemoteState):
+ """Minimax Player."""
+ __slots__ = ()
-def run() -> None:
- """Run MinimaxPlayer clients in local server."""
- import random
-
- random.seed(0)
-
- state = (State.new_game(2), 0)
-
- while not AzulMinimax.terminal(state):
- action = AzulMinimax.adaptive_depth_minimax(state)
- print(f"{action = }")
- state = AzulMinimax.result(state, action.action)
- print(f"{state = }")
- print(state)
+ async def preform_turn(self) -> Action:
+ """Perform turn."""
+ print("preform_turn")
+ ##value, action = CheckersMinimax.adaptive_depth_minimax(
+ ## self.state, 4, 5
+ ##)
+ ##value, action = CheckersMinimax.minimax(self.state, 4)
+ value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 1)
+ ## value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 4)
+ if action is None:
+ raise ValueError("action is None")
+ print(f"{value = }")
+ return action
-## run_clients_in_local_servers_sync(MinimaxPlayer)
+def run() -> None:
+ """Run MinimaxPlayer clients in local server."""
+ ## import random
+ ##
+ ## random.seed(0)
+ ##
+ ## state = (State.new_game(2), 0)
+ ##
+ ## while not AzulMinimax.terminal(state):
+ ## action = AzulMinimax.adaptive_depth_minimax(state)
+ ## print(f"{action = }")
+ ## state = AzulMinimax.result(state, action.action)
+ ## print(f"{state = }")
+ ## print(state)
+
+ run_clients_in_local_servers_sync(MinimaxPlayer)
if __name__ == "__main__":
diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py
index 78254ce..8b85f39 100644
--- a/computer_players/machine_client.py
+++ b/computer_players/machine_client.py
@@ -9,21 +9,39 @@
import sys
from abc import ABCMeta, abstractmethod
from contextlib import asynccontextmanager
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, TypeAlias
import trio
-from checkers.client import GameClient, read_advertisements
-from checkers.component import (
+from libcomponent.component import (
Component,
ComponentManager,
Event,
ExternalRaiseManager,
)
-from checkers.state import Action, Pos, State
+
+from azul.client import GameClient, read_advertisements
+from azul.state import (
+ PatternLine,
+ Phase,
+ SelectableDestination,
+ SelectableDestinationTiles,
+ SelectableSource,
+ SelectableSourceTiles,
+ State,
+ Tile,
+ factory_displays_deepcopy,
+ player_data_deepcopy,
+)
if TYPE_CHECKING:
+ from collections import Counter
from collections.abc import AsyncGenerator
+ from mypy_extensions import u8
+ from numpy import int8
+ from numpy.typing import NDArray
+
+
if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup
@@ -31,6 +49,12 @@
# 0 = False = Person = MIN = 0, 2
# 1 = True = AI (Us) = MAX = 1, 3
+##Action: TypeAlias = tuple[SelectableSourceTiles, SelectableDestinationTiles]
+Action: TypeAlias = (
+ tuple[SelectableDestinationTiles, ...]
+ | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]]
+)
+
class RemoteState(Component, metaclass=ABCMeta):
"""Remote State.
@@ -39,43 +63,84 @@ class RemoteState(Component, metaclass=ABCMeta):
turn.
"""
- __slots__ = ("has_initial", "moves", "pieces", "playing_as", "state")
+ __slots__ = ("has_initial", "moves", "playing_as", "state")
def __init__(self) -> None:
"""Initialize remote state."""
super().__init__("remote_state")
- self.state = State((8, 8), {})
+ self.state = State.blank()
self.has_initial = False
- self.pieces: dict[Pos, int] = {}
- self.playing_as = 1
+ self.playing_as: u8 = 1
self.moves = 0
def bind_handlers(self) -> None:
"""Register game event handlers."""
self.register_handlers(
{
- "game_action_complete": self.handle_action_complete,
"game_winner": self.handle_game_over,
"game_initial_config": self.handle_initial_config,
"game_playing_as": self.handle_playing_as,
- "gameboard_create_piece": self.handle_create_piece,
+ "game_board_data": self.handle_board_data,
+ "game_pattern_data": self.handle_pattern_data,
+ "game_factory_data": self.handle_factory_data,
+ # "game_cursor_data":
+ "game_table_data": self.handle_table_data,
+ # "game_cursor_set_movement_mode":
+ "game_pattern_current_turn_change": self.handle_pattern_current_turn_change,
+ # "game_cursor_set_destination":
+ "game_floor_data": self.handle_floor_data,
},
)
+ async def apply_select_source(
+ self,
+ selection: SelectableSourceTiles,
+ ) -> None:
+ """Select source."""
+ color = selection.tiles
+ raise NotImplementedError(selection.source)
+ if selection.source == SelectableSource.table_center: # type: ignore[unreachable]
+ return self.cursor_selects_table_center(color)
+ if selection.source == SelectableSource.factory:
+ assert selection.source_id is not None
+ return self.cursor_selects_factory(selection.source_id, color)
+ raise NotImplementedError(selection.source)
+
+ async def apply_select_destination(
+ self,
+ selection: SelectableDestinationTiles,
+ ) -> None:
+ """Select destination."""
+ assert self.state.current_phase == Phase.factory_offer
+ assert not self.state.is_cursor_empty()
+
+ raise NotImplementedError(selection.destination)
+ if selection.destination == SelectableDestination.floor_line: # type: ignore[unreachable]
+ color = self.state.get_cursor_holding_color()
+ return self.player_select_floor_line(
+ color,
+ selection.place_count,
+ )
+ if selection.destination == SelectableDestination.pattern_line:
+ assert selection.destination_id is not None
+ return self.state.player_selects_pattern_line(
+ selection.destination_id,
+ selection.place_count,
+ )
+ raise NotImplementedError(selection.destination)
+
async def preform_action(self, action: Action) -> None:
"""Raise events to perform game action."""
- await self.raise_event(
- Event(
- "gameboard_piece_clicked",
- (
- action.from_pos,
- self.state.pieces[action.from_pos],
- ),
- ),
- )
- await self.raise_event(Event("gameboard_tile_clicked", action.to_pos))
+ source, dest = action
+ assert isinstance(source, SelectableSourceTiles)
+
+ await self.apply_select_source(source)
+ destination = dest[0]
+ assert isinstance(destination, SelectableDestinationTiles)
+ await self.apply_select_destination(destination)
+ raise NotImplementedError(f"{source = } {dest = }")
@abstractmethod
async def preform_turn(self) -> Action:
@@ -84,54 +149,165 @@ async def preform_turn(self) -> Action:
async def base_preform_turn(self) -> None:
"""Perform turn."""
self.moves += 1
- winner = self.state.check_for_win()
- if winner is not None:
+ ## winner = self.state.check_for_win()
+ ## if winner is not None:
+ if self.state.current_phase == Phase.end:
print("Terminal state, not performing turn")
- value = ("Lost", "Won")[winner == self.playing_as]
+ ##value = ("Lost", "Won")[winner == self.playing_as]
+ value = ""
print(f"{value} after {self.moves}")
+ await trio.lowlevel.checkpoint()
return
+ print(f"Move {self.moves}...")
action = await self.preform_turn()
await self.preform_action(action)
- async def handle_action_complete(
- self,
- event: Event[tuple[Pos, Pos, int]],
- ) -> None:
- """Perform action on internal state and perform our turn if possible."""
- from_pos, to_pos, turn = event.data
- action = self.state.action_from_points(from_pos, to_pos)
- self.state = self.state.preform_action(action)
- ## print(f'{turn = }')
- if turn == self.playing_as:
- await self.base_preform_turn()
+ async def handle_playing_as(self, event: Event[u8]) -> None:
+ """Handle client playing as specified player event."""
+ ## print("handle_playing_as")
+ self.playing_as = event.data
- async def handle_create_piece(self, event: Event[tuple[Pos, int]]) -> None:
- """Update internal pieces if we haven't had the initial setup event."""
- if self.has_initial:
+ if self.state.current_turn == self.playing_as:
+ await self.base_preform_turn()
return
- pos, type_ = event.data
- self.pieces[pos] = type_
-
- async def handle_playing_as(self, event: Event[int]) -> None:
- """Handle playing as event."""
- self.playing_as = event.data
+ await trio.lowlevel.checkpoint()
async def handle_initial_config(
self,
- event: Event[tuple[Pos, int]],
+ event: Event[tuple[u8, u8, u8, u8, NDArray[int8]]],
) -> None:
- """Set up initial state and perform our turn if possible."""
- board_size, turn = event.data
- self.state = State(board_size, self.pieces, bool(turn))
+ """Set up initial game state."""
+ ## print("handle_initial_config")
+ (
+ variant_play,
+ player_count,
+ factory_count,
+ current_turn,
+ floor_line_data,
+ ) = event.data
+ self.state = State.new_game(player_count, bool(variant_play))
+ self.state = self.state._replace(current_turn=current_turn)
self.has_initial = True
- if turn == self.playing_as:
- await self.base_preform_turn()
+ ##if current_turn == self.playing_as:
+ ## await self.base_preform_turn()
- async def handle_game_over(self, event: Event[int]) -> None:
+ async def handle_game_over(self, event: Event[u8]) -> None:
"""Raise network_stop event so we disconnect from server."""
+ ## print("handle_game_over")
self.has_initial = False
await self.raise_event(Event("network_stop", None))
+ async def handle_board_data(
+ self,
+ event: Event[tuple[u8, NDArray[int8]]],
+ ) -> None:
+ """Handle player board data update."""
+ ## print("handle_board_data")
+ player_id, board_data = event.data
+
+ current_player_data = self.state.player_data[player_id]
+
+ new_player_data = current_player_data._replace(wall=board_data)
+
+ player_data = player_data_deepcopy(self.state.player_data)
+ player_data[player_id] = new_player_data
+
+ self.state = self.state._replace(
+ player_data=player_data,
+ )
+ await trio.lowlevel.checkpoint()
+
+ async def handle_pattern_data(
+ self,
+ event: Event[tuple[u8, u8, tuple[u8, u8]]],
+ ) -> None:
+ """Handle player pattern line data update."""
+ ## print("handle_pattern_data")
+ player_id, row_id, (tile_color, tile_count) = event.data
+
+ current_player_data = self.state.player_data[player_id]
+
+ new_player_data = current_player_data._replace(
+ lines=current_player_data.replace_pattern_line(
+ current_player_data.lines,
+ row_id,
+ PatternLine(Tile(tile_color), int(tile_count)),
+ ),
+ )
+
+ player_data = player_data_deepcopy(self.state.player_data)
+ player_data[player_id] = new_player_data
+
+ self.state = self.state._replace(
+ player_data=player_data,
+ )
+ await trio.lowlevel.checkpoint()
+
+ async def handle_factory_data(
+ self,
+ event: Event[tuple[u8, Counter[u8]]],
+ ) -> None:
+ """Handle factory data update."""
+ ## print("handle_factory_data")
+ factory_id, tiles = event.data
+
+ factory_displays = factory_displays_deepcopy(
+ self.state.factory_displays,
+ )
+ factory_displays[factory_id] = tiles
+
+ self.state = self.state._replace(
+ factory_displays=factory_displays,
+ )
+ await trio.lowlevel.checkpoint()
+
+ async def handle_table_data(self, event: Event[Counter[u8]]) -> None:
+ """Handle table center tile data update."""
+ ## print("handle_table_data")
+ table_center = event.data
+
+ self.state = self.state._replace(
+ table_center=table_center,
+ )
+ await trio.lowlevel.checkpoint()
+
+ async def handle_pattern_current_turn_change(
+ self,
+ event: Event[u8],
+ ) -> None:
+ """Handle change of current turn."""
+ ## print("handle_pattern_current_turn_change")
+ pattern_id = event.data
+
+ self.state = self.state._replace(
+ current_turn=pattern_id,
+ )
+
+ if self.state.current_turn == self.playing_as:
+ await self.base_preform_turn()
+ return
+ await trio.lowlevel.checkpoint()
+
+ async def handle_floor_data(
+ self,
+ event: Event[tuple[u8, Counter[u8]]],
+ ) -> None:
+ """Handle floor data event."""
+ ## print("handle_floor_data")
+ floor_id, floor_line = event.data
+
+ current_player_data = self.state.player_data[floor_id]
+
+ new_player_data = current_player_data._replace(floor=floor_line)
+
+ player_data = player_data_deepcopy(self.state.player_data)
+ player_data[floor_id] = new_player_data
+
+ self.state = self.state._replace(
+ player_data=player_data,
+ )
+ await trio.lowlevel.checkpoint()
+
class MachineClient(ComponentManager):
"""Manager that runs until client_disconnected event fires."""
diff --git a/computer_players/minimax.py b/computer_players/minimax.py
index 0a4600a..10f9568 100644
--- a/computer_players/minimax.py
+++ b/computer_players/minimax.py
@@ -11,9 +11,10 @@
import operator
import random
from abc import ABC, abstractmethod
+from collections.abc import Callable
from enum import IntEnum, auto
from math import inf as infinity
-from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar
+from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar, cast
if TYPE_CHECKING:
from collections.abc import Iterable
@@ -107,6 +108,7 @@ def minimax(
current_player = cls.player(state)
value: int | float
+ best: Callable[[float, float], float]
if current_player == Player.MAX:
value = -infinity
best = max
@@ -115,7 +117,7 @@ def minimax(
best = min
elif current_player == Player.CHANCE:
value = 0
- best = sum
+ best = cast(Callable[[float, float], float], sum)
else:
raise ValueError(f"Unexpected player type {current_player!r}")
@@ -156,6 +158,9 @@ def alphabeta(
current_player = cls.player(state)
value: int | float
+ best: Callable[[float, float], float]
+ compare = operator.gt
+ set_idx = 0
if current_player == Player.MAX:
value = -infinity
best = max
@@ -168,7 +173,7 @@ def alphabeta(
set_idx = 1
elif current_player == Player.CHANCE:
value = 0
- best = sum
+ best = cast(Callable[[float, float], float], sum)
else:
raise ValueError(f"Unexpected player type {current_player!r}")
diff --git a/pyproject.toml b/pyproject.toml
index 3ddaa43..8e4ece1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -60,7 +60,7 @@ azul = ["py.typed", "data/*"]
[tool.mypy]
plugins = ["numpy.typing.mypy_plugin"]
-files = ["src/azul/",]
+files = ["src/azul/", "computer_players"]
check_untyped_defs = true
disallow_any_decorated = true
disallow_any_generics = true
From 4f19a8cefa847251debc3ff969bc837e0a15f2ae Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Tue, 7 Jan 2025 16:22:51 -0600
Subject: [PATCH 33/67] More work on minimax AI
---
computer_players/MiniMax_AI.py | 67 +++++++++++--
computer_players/machine_client.py | 147 ++++++++++++++++++++++-------
src/azul/game.py | 6 +-
src/azul/server.py | 8 +-
src/azul/state.py | 27 +++---
5 files changed, 196 insertions(+), 59 deletions(-)
diff --git a/computer_players/MiniMax_AI.py b/computer_players/MiniMax_AI.py
index 3da2d9c..2e290a8 100755
--- a/computer_players/MiniMax_AI.py
+++ b/computer_players/MiniMax_AI.py
@@ -11,10 +11,12 @@
__author__ = "CoolCat467"
__version__ = "0.0.0"
+from math import inf as infinity
from typing import TYPE_CHECKING, TypeAlias, TypeVar
from machine_client import RemoteState, run_clients_in_local_servers_sync
from minimax import Minimax, MinimaxResult, Player
+from mypy_extensions import u8
from azul.state import (
Phase,
@@ -26,6 +28,8 @@
if TYPE_CHECKING:
from collections.abc import Iterable
+ from typing_extensions import Self
+
T = TypeVar("T")
Action: TypeAlias = (
tuple[SelectableDestinationTiles, ...]
@@ -37,13 +41,30 @@
# 1 = True = AI (Us) = MAX = 1, 3
-class AzulMinimax(Minimax[tuple[State, int], Action]):
+class AutoWallState(State):
+ """Azul State with automatic wall tiling in regular play mode."""
+
+ __slots__ = ()
+
+ def _factory_offer_maybe_next_turn(self) -> Self:
+ """Return either current state or new state if player's turn is over."""
+ new_state = super()._factory_offer_maybe_next_turn()
+
+ if (
+ new_state.current_phase == Phase.wall_tiling
+ and not new_state.variant_play
+ ):
+ return new_state.apply_auto_wall_tiling()
+ return new_state
+
+
+class AzulMinimax(Minimax[tuple[AutoWallState, u8], Action]):
"""Minimax Algorithm for Checkers."""
__slots__ = ()
@staticmethod
- def value(state: tuple[State, int]) -> int | float:
+ def value(state: tuple[AutoWallState, u8]) -> int | float:
"""Return value of given game state."""
# Real
real_state, max_player = state
@@ -68,13 +89,13 @@ def value(state: tuple[State, int]) -> int | float:
return (max_ - min_) / (abs(max_) + abs(min_) + 1)
@staticmethod
- def terminal(state: tuple[State, int]) -> bool:
+ def terminal(state: tuple[AutoWallState, u8]) -> bool:
"""Return if game state is terminal."""
real_state, _max_player = state
return real_state.current_phase == Phase.end
@staticmethod
- def player(state: tuple[State, int]) -> Player:
+ def player(state: tuple[AutoWallState, u8]) -> Player:
"""Return Player enum from current state's turn."""
real_state, max_player = state
return (
@@ -82,14 +103,17 @@ def player(state: tuple[State, int]) -> Player:
)
@staticmethod
- def actions(state: tuple[State, int]) -> Iterable[Action]:
+ def actions(state: tuple[AutoWallState, u8]) -> Iterable[Action]:
"""Return all actions that are able to be performed for the current player in the given state."""
real_state, _max_player = state
return tuple(real_state.yield_actions())
## print(f'{len(actions) = }')
@staticmethod
- def result(state: tuple[State, int], action: Action) -> tuple[State, int]:
+ def result(
+ state: tuple[AutoWallState, u8],
+ action: Action,
+ ) -> tuple[AutoWallState, u8]:
"""Return new state after performing given action on given current state."""
real_state, max_player = state
return (real_state.preform_action(action), max_player)
@@ -97,19 +121,43 @@ def result(state: tuple[State, int], action: Action) -> tuple[State, int]:
@classmethod
def adaptive_depth_minimax(
cls,
- state: tuple[State, int],
+ state: tuple[AutoWallState, u8],
) -> MinimaxResult[Action]:
"""Adaptive depth minimax."""
# TODO
depth = 1
return cls.alphabeta(state, depth)
+ @classmethod
+ def alphabeta(
+ cls,
+ state: tuple[AutoWallState, u8],
+ depth: int | None = 5,
+ a: int | float = -infinity,
+ b: int | float = infinity,
+ ) -> MinimaxResult[
+ tuple[SelectableDestinationTiles, ...]
+ | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]]
+ ]:
+ """Return minimax alphabeta pruning result best action for given current state."""
+ new_state, player = state
+ if (
+ new_state.current_phase == Phase.wall_tiling
+ and not new_state.variant_play
+ ):
+ new_state = new_state.apply_auto_wall_tiling()
+ return super().alphabeta((new_state, player), depth, a, b)
+
class MinimaxPlayer(RemoteState):
"""Minimax Player."""
__slots__ = ()
+ def __init__(self) -> None:
+ """Initialize remote minmax player state."""
+ super().__init__(state_class=AutoWallState)
+
async def preform_turn(self) -> Action:
"""Perform turn."""
print("preform_turn")
@@ -117,7 +165,8 @@ async def preform_turn(self) -> Action:
## self.state, 4, 5
##)
##value, action = CheckersMinimax.minimax(self.state, 4)
- value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 1)
+ assert isinstance(self.state, AutoWallState)
+ value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 2)
## value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 4)
if action is None:
raise ValueError("action is None")
@@ -131,7 +180,7 @@ def run() -> None:
##
## random.seed(0)
##
- ## state = (State.new_game(2), 0)
+ ## state = (AutoWallState.new_game(2), 0)
##
## while not AzulMinimax.terminal(state):
## action = AzulMinimax.adaptive_depth_minimax(state)
diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py
index 8b85f39..5a616f1 100644
--- a/computer_players/machine_client.py
+++ b/computer_players/machine_client.py
@@ -9,7 +9,7 @@
import sys
from abc import ABCMeta, abstractmethod
from contextlib import asynccontextmanager
-from typing import TYPE_CHECKING, TypeAlias
+from typing import TYPE_CHECKING, TypeAlias, cast
import trio
from libcomponent.component import (
@@ -32,6 +32,7 @@
factory_displays_deepcopy,
player_data_deepcopy,
)
+from azul.vector import Vector2
if TYPE_CHECKING:
from collections import Counter
@@ -63,18 +64,29 @@ class RemoteState(Component, metaclass=ABCMeta):
turn.
"""
- __slots__ = ("has_initial", "moves", "playing_as", "state")
+ __slots__ = (
+ "can_made_play",
+ "has_initial",
+ "moves",
+ "playing_as",
+ "playing_lock",
+ "state",
+ )
- def __init__(self) -> None:
+ def __init__(self, state_class: type[State] = State) -> None:
"""Initialize remote state."""
super().__init__("remote_state")
- self.state = State.blank()
+ ## print(f'[RemoteState] {state_class = }')
+ self.state = state_class.blank()
self.has_initial = False
self.playing_as: u8 = 1
self.moves = 0
+ self.playing_lock = trio.Lock()
+ self.can_made_play = True
+
def bind_handlers(self) -> None:
"""Register game event handlers."""
self.register_handlers(
@@ -85,7 +97,7 @@ def bind_handlers(self) -> None:
"game_board_data": self.handle_board_data,
"game_pattern_data": self.handle_pattern_data,
"game_factory_data": self.handle_factory_data,
- # "game_cursor_data":
+ "game_cursor_data": self.handle_cursor_data,
"game_table_data": self.handle_table_data,
# "game_cursor_set_movement_mode":
"game_pattern_current_turn_change": self.handle_pattern_current_turn_change,
@@ -99,14 +111,17 @@ async def apply_select_source(
selection: SelectableSourceTiles,
) -> None:
"""Select source."""
+ ## print(f"select {selection = }")
color = selection.tiles
- raise NotImplementedError(selection.source)
- if selection.source == SelectableSource.table_center: # type: ignore[unreachable]
- return self.cursor_selects_table_center(color)
- if selection.source == SelectableSource.factory:
+ if selection.source == SelectableSource.table_center:
+ await self.raise_event(Event("game_table_clicked", color))
+ elif selection.source == SelectableSource.factory:
assert selection.source_id is not None
- return self.cursor_selects_factory(selection.source_id, color)
- raise NotImplementedError(selection.source)
+ await self.raise_event(
+ Event("game_factory_clicked", (selection.source_id, color)),
+ )
+ else:
+ raise NotImplementedError(selection.source)
async def apply_select_destination(
self,
@@ -114,33 +129,68 @@ async def apply_select_destination(
) -> None:
"""Select destination."""
assert self.state.current_phase == Phase.factory_offer
- assert not self.state.is_cursor_empty()
-
- raise NotImplementedError(selection.destination)
- if selection.destination == SelectableDestination.floor_line: # type: ignore[unreachable]
- color = self.state.get_cursor_holding_color()
- return self.player_select_floor_line(
- color,
- selection.place_count,
+ ##assert not self.state.is_cursor_empty()
+ ## print(f'dest {selection = }')
+
+ if selection.destination == SelectableDestination.floor_line:
+ await self.raise_event(
+ Event(
+ "game_floor_clicked",
+ (self.playing_as, selection.place_count),
+ ),
)
- if selection.destination == SelectableDestination.pattern_line:
+ elif selection.destination == SelectableDestination.pattern_line:
assert selection.destination_id is not None
- return self.state.player_selects_pattern_line(
- selection.destination_id,
- selection.place_count,
+ line_id = selection.destination_id
+ currently_placed = self.state.get_player_line_current_place_count(
+ line_id,
)
- raise NotImplementedError(selection.destination)
+ await self.raise_event(
+ Event(
+ "game_pattern_row_clicked",
+ (
+ self.playing_as,
+ Vector2(
+ 5 - selection.place_count - currently_placed,
+ line_id,
+ ),
+ ),
+ ),
+ )
+ else:
+ raise NotImplementedError(selection.destination)
async def preform_action(self, action: Action) -> None:
"""Raise events to perform game action."""
- source, dest = action
- assert isinstance(source, SelectableSourceTiles)
+ await self.raise_event(
+ Event(
+ "game_cursor_location_transmit",
+ Vector2(0.5, 0.5),
+ ),
+ )
+ source: SelectableSourceTiles | None = None
+ dest: tuple[SelectableDestinationTiles, ...]
+ if len(action) == 2:
+ raw_source, raw_dest = action
+ if isinstance(raw_source, SelectableSourceTiles):
+ source = raw_source
+ dest = cast(tuple[SelectableDestinationTiles, ...], raw_dest)
+ else:
+ dest = cast(tuple[SelectableDestinationTiles, ...], action)
+ else:
+ dest = action
+
+ async with self.playing_lock:
+ self.can_made_play = False
+ if source is not None:
+ await self.apply_select_source(source)
+ for destination in dest:
+ ## print(f'{destination = }')
+ assert isinstance(destination, SelectableDestinationTiles)
+ await self.apply_select_destination(destination)
+ self.can_made_play = True
- await self.apply_select_source(source)
- destination = dest[0]
- assert isinstance(destination, SelectableDestinationTiles)
- await self.apply_select_destination(destination)
- raise NotImplementedError(f"{source = } {dest = }")
+ ## raise NotImplementedError(f"{source = } {dest = }")
@abstractmethod
async def preform_turn(self) -> Action:
@@ -148,6 +198,12 @@ async def preform_turn(self) -> Action:
async def base_preform_turn(self) -> None:
"""Perform turn."""
+ ## async with self.playing_lock:
+ if not self.can_made_play:
+ print("Skipping making move because of flag.")
+ await trio.lowlevel.checkpoint()
+ return
+ self.can_made_play = False
self.moves += 1
## winner = self.state.check_for_win()
## if winner is not None:
@@ -158,13 +214,14 @@ async def base_preform_turn(self) -> None:
print(f"{value} after {self.moves}")
await trio.lowlevel.checkpoint()
return
- print(f"Move {self.moves}...")
+ print(f"\nMove {self.moves}...")
action = await self.preform_turn()
await self.preform_action(action)
+ print("Action complete.")
async def handle_playing_as(self, event: Event[u8]) -> None:
"""Handle client playing as specified player event."""
- ## print("handle_playing_as")
+ print("handle_playing_as")
self.playing_as = event.data
if self.state.current_turn == self.playing_as:
@@ -185,6 +242,7 @@ async def handle_initial_config(
current_turn,
floor_line_data,
) = event.data
+ ## print(f'[RemoteState] {variant_play = }')
self.state = State.new_game(player_count, bool(variant_play))
self.state = self.state._replace(current_turn=current_turn)
self.has_initial = True
@@ -259,6 +317,27 @@ async def handle_factory_data(
self.state = self.state._replace(
factory_displays=factory_displays,
)
+
+ ##if self.state.current_turn == self.playing_as:
+ ## await self.base_preform_turn()
+ ## return
+ await trio.lowlevel.checkpoint()
+
+ async def handle_cursor_data(
+ self,
+ event: Event[Counter[u8]],
+ ) -> None:
+ """Handle cursor data update."""
+ ## print("handle_cursor_data")
+ cursor_contents = event.data
+
+ self.state = self.state._replace(
+ cursor_contents=cursor_contents,
+ )
+
+ ## if self.state.current_turn == self.playing_as and not self.state.is_cursor_empty():
+ ## await self.base_preform_turn()
+ ## return
await trio.lowlevel.checkpoint()
async def handle_table_data(self, event: Event[Counter[u8]]) -> None:
@@ -276,7 +355,7 @@ async def handle_pattern_current_turn_change(
event: Event[u8],
) -> None:
"""Handle change of current turn."""
- ## print("handle_pattern_current_turn_change")
+ print("handle_pattern_current_turn_change")
pattern_id = event.data
self.state = self.state._replace(
diff --git a/src/azul/game.py b/src/azul/game.py
index ece5b38..f03ea2a 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -2167,8 +2167,10 @@ def cli_run() -> None:
)
run()
except ExceptionGroup as exc:
- print(exc)
- exception = "".join(traceback.format_exception(exc))
+ ## print(exc)
+ ## exception = "".join(traceback.format_exception(exc))
+ ## print(exception)
+ traceback.print_exception(exc)
## raise
## except BaseException as ex:
## screenshot_last_frame()
diff --git a/src/azul/server.py b/src/azul/server.py
index 214ad94..ef9f7f4 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -1250,8 +1250,10 @@ async def handle_client_pattern_row_clicked(
),
)
+ did_auto_wall_tile = False
if self.state.current_phase == Phase.wall_tiling:
if not self.state.variant_play:
+ did_auto_wall_tile = True
self.state = self.state.apply_auto_wall_tiling()
await self.transmit_new_round_data()
await self.transmit_pattern_line_data()
@@ -1259,7 +1261,7 @@ async def handle_client_pattern_row_clicked(
if self.state.current_phase == Phase.end:
print("TODO: Handle end of game.")
- if self.state.current_turn != player_id:
+ if self.state.current_turn != player_id or did_auto_wall_tile:
await self.raise_event(
Event(
"current_turn_change->network",
@@ -1388,8 +1390,10 @@ async def handle_client_floor_clicked(
),
)
+ did_auto_wall_tile = False
if self.state.current_phase == Phase.wall_tiling:
if not self.state.variant_play:
+ did_auto_wall_tile = True
self.state = self.state.apply_auto_wall_tiling()
await self.transmit_new_round_data()
await self.transmit_pattern_line_data()
@@ -1397,7 +1401,7 @@ async def handle_client_floor_clicked(
if self.state.current_phase == Phase.end:
print("TODO: Handle end of game.")
- if self.state.current_turn != player_id:
+ if self.state.current_turn != player_id or did_auto_wall_tile:
await self.raise_event(
Event(
"current_turn_change->network",
diff --git a/src/azul/state.py b/src/azul/state.py
index c801135..1182aea 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -985,25 +985,26 @@ def yield_selectable_tile_destinations_factory_offer(
current_player_data = self.player_data[self.current_turn]
color = self.get_cursor_holding_color()
- count = self.cursor_contents[color] + 1
+ count = self.cursor_contents[color]
for (
line_id,
placable,
) in current_player_data.yield_possible_placement_rows(color):
- for place_count in range(1, min(count, placable + 1)):
- yield SelectableDestinationTiles(
- destination=SelectableDestination.pattern_line,
- place_count=place_count,
- destination_id=line_id,
- )
- # Can always place in floor line, even if full,
- # because of box lid overflow
- for place_count in range(1, count):
+ ## for place_count in range(1, min(count, placable + 1)):
+ place_count = min(count, placable)
yield SelectableDestinationTiles(
- destination=SelectableDestination.floor_line,
+ destination=SelectableDestination.pattern_line,
place_count=place_count,
+ destination_id=line_id,
)
+ # Can always place in floor line, even if full,
+ # because of box lid overflow
+ ## for place_count in range(1, count):
+ yield SelectableDestinationTiles(
+ destination=SelectableDestination.floor_line,
+ place_count=count,
+ )
def apply_destination_select_action_factory_offer(
self,
@@ -1139,8 +1140,10 @@ def yield_actions(
action_chain
) in new.yield_all_factory_offer_destinations():
yield (selection, action_chain)
+ elif self.current_phase == Phase.end:
+ pass
else:
- raise NotImplementedError()
+ raise NotImplementedError(f"{self.current_phase = }")
def preform_action(
self,
From 3405ffe291a322deb7681eb4bc9ef0dd49a49051 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 11 Jan 2025 16:45:21 -0600
Subject: [PATCH 34/67] Replace language module with `database` from
MineOS-Market-Server Adding orjson as runtime dependency instead of only test
dependency
---
pyproject.toml | 9 +-
src/azul/database.py | 329 +++++++++++++++++++++++++++++++++++++++++++
src/azul/lang.py | 30 ----
test-requirements.in | 8 +-
4 files changed, 336 insertions(+), 40 deletions(-)
create mode 100644 src/azul/database.py
delete mode 100644 src/azul/lang.py
diff --git a/pyproject.toml b/pyproject.toml
index 8e4ece1..407bade 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,13 +36,14 @@ keywords = [
"ai", "multi-player", "azul", "ai-support", "networked-game"
]
dependencies = [
+ "exceptiongroup; python_version < '3.11'",
"libcomponent~=0.0.1",
- "pygame~=2.6.0",
- "typing_extensions>=4.12.2",
"mypy_extensions>=1.0.0",
- "trio~=0.28.0",
- "exceptiongroup; python_version < '3.11'",
"numpy~=2.1.3",
+ "orjson>=3.10,<4",
+ "pygame~=2.6.0",
+ "trio~=0.28.0",
+ "typing_extensions>=4.12.2",
]
[tool.setuptools.dynamic]
diff --git a/src/azul/database.py b/src/azul/database.py
new file mode 100644
index 0000000..8d19b2a
--- /dev/null
+++ b/src/azul/database.py
@@ -0,0 +1,329 @@
+"""Database - Read and write json files."""
+
+# Programmed by CoolCat467
+
+from __future__ import annotations
+
+# Database - Read and write json files
+# Copyright (C) 2024 CoolCat467
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+__title__ = "Database"
+__author__ = "CoolCat467"
+
+from os import makedirs, path
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+import orjson
+import trio
+
+if TYPE_CHECKING:
+ from collections.abc import Generator, Iterable, Iterator
+ from types import TracebackType
+
+ from typing_extensions import Self
+
+
+_LOADED: dict[str, Records] = {}
+
+
+class Database(dict[str, Any]):
+ """Database dict with file read write functions."""
+
+ __slots__ = ("__weakref__", "file")
+
+ def __init__(
+ self,
+ file_path: str | Path | trio.Path,
+ auto_load: bool = True,
+ ) -> None:
+ """Initialize and set file path.
+
+ If auto_load is True, automatically load file contents synchronously
+ if file exists.
+ """
+ super().__init__()
+ self.file = file_path
+
+ if auto_load and path.exists(self.file):
+ self.reload_file()
+
+ def reload_file(self) -> None:
+ """Reload database file.
+
+ Will raise FileNotFoundError in the event file does not exist.
+ """
+ self.update(orjson.loads(Path(self.file).read_bytes()))
+
+ async def reload_async(self) -> None:
+ """Reload database file asynchronously.
+
+ Does not decode json data if file is empty.
+ Will raise FileNotFoundError in the event file does not exist.
+ """
+ async with await trio.open_file(self.file, "rb") as file:
+ data = await file.read()
+ if not data:
+ return
+ self.update(orjson.loads(data))
+
+ def serialize(self) -> bytes:
+ """Return this object's data serialized as bytes."""
+ return orjson.dumps(
+ self,
+ option=orjson.OPT_APPEND_NEWLINE
+ | orjson.OPT_NON_STR_KEYS
+ | orjson.OPT_NAIVE_UTC,
+ )
+
+ def write_file(self) -> None:
+ """Write database file.
+
+ May raise PermissionError in the event of insufficient permissions.
+ """
+ folder = path.dirname(self.file)
+ if not path.exists(folder):
+ makedirs(folder, exist_ok=False)
+ Path(self.file).write_bytes(self.serialize())
+
+ async def write_async(self) -> None:
+ """Write database file asynchronously.
+
+ May raise PermissionError in the event of insufficient permissions.
+ """
+ folder = trio.Path(self.file).parent
+ if not await folder.exists():
+ await folder.mkdir(parents=True, exist_ok=False)
+ async with await trio.open_file(
+ self.file,
+ "wb",
+ ) as file:
+ await file.write(self.serialize())
+
+ def __enter__(self) -> Self:
+ """Enter context manager."""
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ """Context manager exit."""
+ self.write_file()
+
+ async def __aenter__(self) -> Self:
+ """Enter async context manager.
+
+ Automatically reloads file if it exists.
+ """
+ if await trio.Path(self.file).exists():
+ await self.reload_async()
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ """Async context manager exit, write file contents asynchronously."""
+ await self.write_async()
+
+
+class Table:
+ """Table from dictionary.
+
+ Allows getting and setting entire columns of a database
+ """
+
+ __slots__ = ("_key_name", "_records")
+
+ def __init__(self, records: dict[str, Any], key_name: str) -> None:
+ """Initialize and set records and key name."""
+ self._records = records
+ self._key_name = key_name
+
+ def __repr__(self) -> str:
+ """Get text representation of table."""
+ size: dict[str, int] = {}
+ columns = self.keys()
+ for column in columns:
+ size[column] = len(column)
+ for value in self[column]:
+ if value is None:
+ continue
+ length = (
+ len(value)
+ if hasattr(value, "__len__")
+ else len(repr(value))
+ )
+ size[column] = max(size[column], length)
+ num_pad = len(str(len(self)))
+ lines = []
+ column_names = " ".join(c.ljust(length) for c, length in size.items())
+ lines.append("".rjust(num_pad) + " " + column_names)
+ for index in range(len(self)):
+ line = [str(index).ljust(num_pad)]
+ for column in columns:
+ line.append(str(self[column][index]).ljust(size[column]))
+ lines.append(" ".join(line))
+ return "\n".join(lines)
+
+ def __getitem__(self, column: str) -> tuple[Any, ...]:
+ """Get column data."""
+ if column not in self.keys():
+ return tuple(None for _ in range(len(self)))
+ if column == self._key_name:
+ return tuple(self._records.keys())
+ return tuple(row.get(column) for row in self._records.values())
+
+ def __setitem__(self, column: str, value: Iterable[Any]) -> None:
+ """Set column data to value."""
+ if column == self._key_name:
+ for old, new in zip(tuple(self._records), value, strict=False):
+ self._records[new] = self._records.pop(old)
+ else:
+ for key, set_value in zip(self._records, value, strict=True):
+ if set_value is None:
+ continue
+ self._records[key][column] = set_value
+
+ def _raw_keys(self) -> set[str]:
+ """Return the name of every column."""
+ keys = set()
+ for row in self._records.values():
+ keys |= set(row.keys())
+ return keys
+
+ def keys(self) -> set[str]:
+ """Return the name of every column."""
+ return self._raw_keys() | {self._key_name}
+
+ def __iter__(self) -> Iterator[str]:
+ """Return iterator for column names."""
+ return iter(self.keys())
+
+ def values(self) -> tuple[Any, ...]:
+ """Return every column."""
+ values = []
+ for key in self.keys():
+ values.append(self[key])
+ return tuple(values)
+
+ def items(self) -> tuple[tuple[str, Any], ...]:
+ """Return tuples of column names and columns."""
+ items = []
+ for key in sorted(self.keys()):
+ items.append((key, self[key]))
+ return tuple(items)
+
+ def _rows(
+ self,
+ columns: list[str],
+ ) -> Generator[tuple[Any, ...], None, None]:
+ """Yield columns in order from each row."""
+ for key, value in self._records.items():
+ yield (key, *tuple(value.get(col) for col in columns))
+
+ def rows(self) -> Generator[tuple[Any, ...], None, None]:
+ """Yield each row."""
+ yield from self._rows(sorted(self.keys()))
+
+ def column_and_rows(self) -> Generator[tuple[str | Any, ...], None, None]:
+ """Yield tuple of column row and then rows in column order."""
+ columns = sorted(self._raw_keys())
+ yield (self._key_name, *columns)
+ yield from self._rows(columns)
+
+ def __len__(self) -> int:
+ """Return number of records."""
+ return len(self._records)
+
+ def get_id(self, key: str, value: object) -> int | None:
+ """Return index of value in column key or None if not found."""
+ try:
+ return self[key].index(value)
+ except ValueError:
+ return None
+
+
+class Records(Database):
+ """Records dict with columns."""
+
+ __slots__ = ()
+
+ def table(self, element_name: str) -> Table:
+ """Get table object given that keys are named element name."""
+ return Table(self, element_name)
+
+
+def load(file_path: str | Path | trio.Path) -> Records:
+ """Load database from file path or return already loaded instance."""
+ file = path.abspath(file_path)
+ if file not in _LOADED:
+ _LOADED[file] = Records(file)
+ return _LOADED[file]
+
+
+async def load_async(file_path: str | Path | trio.Path) -> Records:
+ """Load database from file path or return already loaded instance."""
+ await trio.lowlevel.checkpoint()
+ file = path.abspath(file_path)
+ if file not in _LOADED:
+ _LOADED[file] = Records(file, auto_load=False)
+ if await trio.Path(file).exists():
+ await _LOADED[file].reload_async()
+ return _LOADED[file]
+
+
+def get_loaded() -> set[str]:
+ """Return set of loaded database files."""
+ return set(_LOADED)
+
+
+def unload(file_path: str | Path | trio.Path) -> None:
+ """If database loaded, write file and unload."""
+ file = path.abspath(file_path)
+ if file not in get_loaded():
+ return
+ database = load(file)
+ database.write_file()
+ del _LOADED[file]
+
+
+async def async_unload(file_path: str | Path | trio.Path) -> None:
+ """If database loaded, write file and unload."""
+ file = path.abspath(file_path)
+ if file not in get_loaded():
+ return
+ database = load(file)
+ await database.write_async()
+ del _LOADED[file]
+
+
+def unload_all() -> None:
+ """Unload all loaded databases."""
+ for file_path in get_loaded():
+ unload(file_path)
+
+
+async def async_unload_all() -> None:
+ """Unload all loaded databases."""
+ async with trio.open_nursery() as nursery:
+ for file_path in get_loaded():
+ nursery.start_soon(async_unload, file_path)
diff --git a/src/azul/lang.py b/src/azul/lang.py
deleted file mode 100644
index b97d6c0..0000000
--- a/src/azul/lang.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Language file handler."""
-
-from __future__ import annotations
-
-# Programmed by CoolCat467
-
-__title__ = "lang"
-__author__ = "CoolCat467"
-__version__ = "0.0.0"
-
-import json
-from functools import cache
-from os.path import exists, join
-
-
-def load_json(filename: str) -> dict[str, str]:
- """Return json data loaded from filename."""
- with open(filename, encoding="utf-8") as loaded:
- data = json.load(loaded)
- assert isinstance(data, dict)
- return data
-
-
-@cache
-def load_lang(name: str) -> dict[str, str] | None:
- """Return full data for language with given name."""
- filename = join("lang", f"{name}.json")
- if not exists(filename):
- return None
- return load_json(filename)
diff --git a/test-requirements.in b/test-requirements.in
index 1e6d53e..105217b 100644
--- a/test-requirements.in
+++ b/test-requirements.in
@@ -6,22 +6,18 @@ pytest-cov
# Tools
black; implementation_name == "cpython"
-mypy >= 1.14.0 # Would use mypy[faster-cache], but orjson has build issues on pypy
-orjson; implementation_name == "cpython"
+mypy >= 1.14.0
ruff >= 0.6.6
uv >= 0.2.24
codespell
-# https://github.com/python-trio/trio/pull/654#issuecomment-420518745
-mypy-extensions
-typing-extensions
-
# Azul's own dependencies
#
exceptiongroup; python_version < '3.11'
libcomponent~=0.0.1
mypy_extensions>=1.0.0
numpy~=2.1.3
+orjson>=3.10,<4
pygame~=2.6.0
trio~=0.28.0
typing_extensions>=4.12.2
From 336de9809eeae98b3acc60beb8e14c7e5d9319cb Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 11 Jan 2025 18:03:00 -0600
Subject: [PATCH 35/67] Use localization files for user-displayed strings not
counting debug text
---
pyproject.toml | 2 +-
src/azul/client.py | 18 +++---
src/azul/conf.py | 32 -----------
src/azul/conf/main.conf | 6 --
src/azul/data/tiles/black.png | Bin 72 -> 0 bytes
src/azul/data/tiles/blue.png | Bin 82 -> 0 bytes
src/azul/data/tiles/cyan.png | Bin 82 -> 0 bytes
src/azul/data/tiles/grey.png | Bin 82 -> 0 bytes
src/azul/data/tiles/grey_black.png | Bin 82 -> 0 bytes
src/azul/data/tiles/grey_blue.png | Bin 82 -> 0 bytes
src/azul/data/tiles/grey_cyan.png | Bin 82 -> 0 bytes
src/azul/data/tiles/grey_red.png | Bin 82 -> 0 bytes
src/azul/data/tiles/grey_yellow.png | Bin 82 -> 0 bytes
src/azul/data/tiles/number_one.png | Bin 167 -> 0 bytes
src/azul/data/tiles/red.png | Bin 80 -> 0 bytes
src/azul/data/tiles/yellow.png | Bin 81 -> 0 bytes
src/azul/game.py | 84 +++++++++++++++++++++-------
src/azul/lang/en_us.json | 37 ++++++------
src/azul/mr_floppy_test.py | 4 +-
19 files changed, 92 insertions(+), 91 deletions(-)
delete mode 100644 src/azul/conf.py
delete mode 100644 src/azul/conf/main.conf
delete mode 100644 src/azul/data/tiles/black.png
delete mode 100644 src/azul/data/tiles/blue.png
delete mode 100644 src/azul/data/tiles/cyan.png
delete mode 100644 src/azul/data/tiles/grey.png
delete mode 100644 src/azul/data/tiles/grey_black.png
delete mode 100644 src/azul/data/tiles/grey_blue.png
delete mode 100644 src/azul/data/tiles/grey_cyan.png
delete mode 100644 src/azul/data/tiles/grey_red.png
delete mode 100644 src/azul/data/tiles/grey_yellow.png
delete mode 100644 src/azul/data/tiles/number_one.png
delete mode 100644 src/azul/data/tiles/red.png
delete mode 100644 src/azul/data/tiles/yellow.png
diff --git a/pyproject.toml b/pyproject.toml
index 407bade..c790d1f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -57,7 +57,7 @@ version = {attr = "azul.game.__version__"}
azul_game = "azul.game:cli_run"
[tool.setuptools.package-data]
-azul = ["py.typed", "data/*"]
+azul = ["py.typed", "data/*", "lang/*", "fonts/*"]
[tool.mypy]
plugins = ["numpy.typing.mypy_plugin"]
diff --git a/src/azul/client.py b/src/azul/client.py
index 88d1be8..f604ee1 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -241,16 +241,16 @@ async def print_callback_ping(self, event: Event[int]) -> None:
print(f"[azul.client] print_callback_ping {difference * 1e-06:.03f}ms")
await trio.lowlevel.checkpoint()
- async def raise_disconnect(self, message: str) -> None:
+ async def raise_disconnect(self, message_key: str) -> None:
"""Raise client_disconnected event with given message."""
- print(f"{self.__class__.__name__}: {message}")
+ print(f"{self.__class__.__name__}: {message_key}")
if not self.manager_exists:
print(
f"{self.__class__.__name__}: Manager does not exist, not raising disconnect event.",
)
return
# self.unregister_all_network_write_events()
- await self.raise_event(Event("client_disconnected", message))
+ await self.raise_event(Event("client_disconnected", message_key))
await self.close()
assert self.not_connected
@@ -301,7 +301,7 @@ async def handle_read_event(self) -> None:
if not self.manager_exists:
return
if self.not_connected:
- await self.raise_disconnect("Not connected to server.")
+ await self.raise_disconnect("error.not_connected")
return
# event: Event[bytearray] | None = None
try:
@@ -319,9 +319,7 @@ async def handle_read_event(self) -> None:
print(f"[{self.name}] NetworkTimeoutError")
await self.close()
traceback.print_exception(exc)
- await self.raise_disconnect(
- "Failed to read event from server.",
- )
+ await self.raise_disconnect("error.read_event_fail")
return
except network.NetworkStreamNotConnectedError as exc:
self.running = False
@@ -334,9 +332,7 @@ async def handle_read_event(self) -> None:
self.running = False
print(f"[{self.name}] NetworkEOFError")
await self.close()
- await self.raise_disconnect(
- "Server closed connection.",
- )
+ await self.raise_disconnect("error.socket_eof")
return
## print(f'[azul.client] handle_read_event {event}')
@@ -375,7 +371,7 @@ async def handle_client_connect(
"manager does not exist, cannot send client connection closed event.",
)
return
- await self.raise_disconnect("Error connecting to server.")
+ await self.raise_disconnect("error.socket_connect_fail")
async def read_initial_config(self, event: Event[bytearray]) -> None:
"""Read initial_config event from server."""
diff --git a/src/azul/conf.py b/src/azul/conf.py
deleted file mode 100644
index 4f4400a..0000000
--- a/src/azul/conf.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""Config module."""
-
-# Programmed by CoolCat467
-
-__title__ = "Conf"
-__author__ = "CoolCat467"
-__version__ = "0.0.0"
-
-
-from configparser import ConfigParser
-
-
-def load_config(config_file: str) -> dict[str, dict[str, str]]:
- """Return a config object from config_file."""
- config = ConfigParser()
- config.read((config_file,))
-
- data: dict[str, dict[str, str]] = {}
- for section, values in dict(config.items()).items():
- data[section] = dict(values)
-
- # config.clear()
- # config.update(data)
- ##
- # with open(config_file, mode='w', encoding='utf-8') as conf_file:
- # config.write(conf_file)
-
- return data
-
-
-if __name__ == "__main__":
- print(f"{__title__}\nProgrammed by {__author__}.")
diff --git a/src/azul/conf/main.conf b/src/azul/conf/main.conf
deleted file mode 100644
index f2f1c51..0000000
--- a/src/azul/conf/main.conf
+++ /dev/null
@@ -1,6 +0,0 @@
-[Font]
-font_folder = data
-font_file = RuneScape-UF-Regular.ttf
-
-[Language]
-lang_name = en_us
diff --git a/src/azul/data/tiles/black.png b/src/azul/data/tiles/black.png
deleted file mode 100644
index d19d97136c0b5cc73746c8d148bd289d165e3126..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 72
zcmeAS@N?(olHy`uVBq!ia0vp^{2+Qh)<
T$-Ht4P>R9R)z4*}Q$iB}r<4uT
diff --git a/src/azul/data/tiles/blue.png b/src/azul/data/tiles/blue.png
deleted file mode 100644
index 97cfd8efdeea8a1753b2aedc1dd3e74fa5bab689..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 82
zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2tLjr!BcQ8p%ihH@@
fRp3>VKwgICeT=cY!bP0l+XkK)29{j
diff --git a/src/azul/data/tiles/cyan.png b/src/azul/data/tiles/cyan.png
deleted file mode 100644
index e49b3dbdb2324e3edb5be80928cddff2cd983b1e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 82
zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2tQ-1uP-lAbP(AsQ2tW430AGGA=ytXR5u
em3!!A7gL5dZYCL5QNFuCMGT&lAbP(AsQ2t*Q{Tkt{`O~xV|Lh
f>x!&dE7%yceHdq}h%7h^RK(!v>gTe~DWM4fumTh|
diff --git a/src/azul/data/tiles/grey_blue.png b/src/azul/data/tiles/grey_blue.png
deleted file mode 100644
index 3e5d1a261ca574a0bb88b79c74a03decf0cac8d6..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 82
zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2tbMo(>cQ8p%ihH@@
fRp3>VKwbtWWyaU1F8-VjRK(!v>gTe~DWM4f)u|RD
diff --git a/src/azul/data/tiles/grey_cyan.png b/src/azul/data/tiles/grey_cyan.png
deleted file mode 100644
index 6d4c01f7f761a5e1c67860419ac6d9def9d7aaf8..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 82
zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2t*WBMfzcJ&0%brVt
fR~N6!TqMQNEX^eSL?B`}P!WTttDnm{r-UW|{Qee{
diff --git a/src/azul/data/tiles/grey_red.png b/src/azul/data/tiles/grey_red.png
deleted file mode 100644
index 7ac7401fa16a02e9cea88a68a1c22119922f1ebf..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 82
zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2t@5INScQ8p%ihH@@
fRp3>VKwgIHJ&d=%{VSOPRK(!v>gTe~DWM4f=Ykgn
diff --git a/src/azul/data/tiles/grey_yellow.png b/src/azul/data/tiles/grey_yellow.png
deleted file mode 100644
index 43f00e6e63ab8eaf069521e3b9aa614473cc31a3..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 82
zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2t@9bYYuQB6*%brVt
fR~N6!TqMPioy{b)URa+MsEEPS)z4*}Q$iB}@v9ZD
diff --git a/src/azul/data/tiles/number_one.png b/src/azul/data/tiles/number_one.png
deleted file mode 100644
index 264020879236514c86613dba01363f991129ae74..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 167
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={W@;qG}Lp(Z@Ljr!Bckn*o5`6h(
zA|v;l(*Jg1nXayHZf(#ywyJPJ^NWAAZNI-ue)^o5#`rYhN>d=Wo36xGFr%%VRp-aj
zyLTleBqZ+bJF0i?z<~|L>ZxLPcDc^7P?LC}pmgJa5{HDl^!^s0eKSPXT3)Vr4P-HR
My85}Sb4q9e09g+|*#H0l
diff --git a/src/azul/data/tiles/red.png b/src/azul/data/tiles/red.png
deleted file mode 100644
index 478d2665dfc3a1839f5479959fb2a555adc27f9f..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 80
zcmeAS@N?(olHy`uVBq!ia0vp^{2;+`&!AsQ2t|D1PVN>FnBxZ+jd
dRg(Z-hMT>NvOb$lM1e{eJYD@<);T3K0RWQq6RrRN
diff --git a/src/azul/data/tiles/yellow.png b/src/azul/data/tiles/yellow.png
deleted file mode 100644
index 1341ce2e9fb30ccfde5a76807932042e0b4868c8..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 81
zcmeAS@N?(olHy`uVBq!ia0vp^{25}q!OAsQ2t|J1iPRvd6?x)peJ
e@v6*)QVbfu7=M00`oaXLhQZU-&t;ucLK6V>P#3!Z
diff --git a/src/azul/game.py b/src/azul/game.py
index f03ea2a..9d3140d 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -59,7 +59,7 @@
from pygame.rect import Rect
from pygame.sprite import LayeredDirty
-from azul import element_list, objects, sprite
+from azul import database, element_list, objects, sprite
from azul.async_clock import Clock
from azul.client import GameClient, read_advertisements
from azul.crop import auto_crop_clear
@@ -125,6 +125,9 @@
ROOT_FOLDER: Final = Path(__file__).absolute().parent
DATA_FOLDER: Final = ROOT_FOLDER / "data"
FONT_FOLDER: Final = ROOT_FOLDER / "fonts"
+LANG_FOLDER: Final = ROOT_FOLDER / "lang"
+# TODO: Way to change language
+LANGUAGE: Final = "en_us"
# Game stuff
# Tiles
@@ -154,7 +157,7 @@
GREYSHIFT = 0.75 # 0.65
# Font
-FONT: Final = FONT_FOLDER / "VeraSerif.ttf" # "RuneScape-UF-Regular.ttf"
+FONT: Final = FONT_FOLDER / "VeraSerif.ttf"
SCOREFONTSIZE = 30
BUTTONFONTSIZE = 60
@@ -173,6 +176,45 @@
}
+def decode_localization_entry(localization_string: str) -> list[str]:
+ """Return localization entry path."""
+ return localization_string.split(".")
+
+
+def s_(localization_string: str, **kwargs: object) -> str:
+ """Return localization string entry, or path to it if it doesn't exist."""
+ language_filename = f"{LANGUAGE}.json"
+ language_file = LANG_FOLDER / language_filename
+ # Load keeps copy in-memory, so only performance hit first time.
+ language_data = database.load(language_file)
+
+ localization_entry = decode_localization_entry(localization_string)
+
+ current: dict[str, Any] = language_data
+ new: dict[str, Any] | str | None
+ final: str | None = None
+ for entry in localization_entry:
+ new = current.get(entry)
+ if new is None:
+ break
+ if isinstance(new, str):
+ final = new
+ break
+ assert isinstance(
+ new,
+ dict,
+ ), f"Unexpected value in {language_file!r} for {localization_string!r}"
+ current = new
+ if final is None:
+ # Key does not exist
+ localization_key = f"[{LANGUAGE}] {localization_string}"
+ if kwargs:
+ args = ",".join(f"{k}={v!r}" for k, v in kwargs.items())
+ return f"{localization_key}<{args}>"
+ return localization_key
+ return final.format(**kwargs)
+
+
def vec2_to_location(vec: Vector2) -> tuple[int, int]:
"""Return rounded location tuple from Vector2."""
x, y = map(int, vec.rounded())
@@ -708,7 +750,7 @@ async def handle_client_disconnected(
event: Event[None],
) -> None:
"""Unregister tick event handler."""
- print("[azul.game.Cursor] Got client disconnect, unregistering tick")
+ # print("[azul.game.Cursor] Got client disconnect, unregistering tick")
self.unregister_handler_type("tick")
await trio.lowlevel.checkpoint()
@@ -825,7 +867,11 @@ async def handle_game_board_data(
await trio.lowlevel.checkpoint()
return
- self.data = array
+ assert array.ndim == 2
+ assert len(array.shape) == 2
+ w, h = array.shape
+ # Prove to typechecker that array is 2D
+ self.data = array.reshape((w, h))
self.update_image()
self.visible = True
@@ -1508,7 +1554,7 @@ async def entry_actions(self) -> None:
color=Color(0, 0, 0),
outline=(255, 0, 0),
border_width=4,
- text=__title__.upper(),
+ text=s_("title.game_title"),
)
title_text.location = (SCREEN_SIZE[0] // 2, title_text.rect.h)
self.group_add(title_text)
@@ -1518,7 +1564,7 @@ async def entry_actions(self) -> None:
button_font,
visible=True,
color=Color(0, 0, 0),
- text="Host Networked Game",
+ text=s_("title.host_game"),
location=[x // 2 for x in SCREEN_SIZE],
handle_click=self.change_state("play_hosting"),
)
@@ -1529,7 +1575,7 @@ async def entry_actions(self) -> None:
button_font,
visible=True,
color=Color(0, 0, 0),
- text="Join Networked Game",
+ text=s_("title.join_game"),
location=hosting_button.location
+ Vector2(
0,
@@ -1544,7 +1590,7 @@ async def entry_actions(self) -> None:
button_font,
visible=True,
color=Color(0, 0, 0),
- text="Singleplayer Game",
+ text=s_("title.singleplayer"),
location=hosting_button.location
- Vector2(
0,
@@ -1559,7 +1605,7 @@ async def entry_actions(self) -> None:
button_font,
visible=True,
color=Color(0, 0, 0),
- text="Quit",
+ text=s_("title.quit"),
location=join_button.location
+ Vector2(
0,
@@ -1656,7 +1702,7 @@ def __init__(self, name: str, font: pygame.font.Font) -> None:
self.update_location_on_resize = False
self.border_width = 4
self.outline = RED
- self.text = "Return to Title"
+ self.text = s_("connect.return_title")
self.visible = True
self.location = (SCREEN_SIZE[0] // 2, self.location.y + 10)
@@ -1929,7 +1975,7 @@ async def exit_actions(self) -> None:
async def handle_game_over(self, event: Event[int]) -> None:
"""Handle game over event."""
winner = event.data
- self.exit_data = (0, f"{winner} Won", False)
+ self.exit_data = (0, s_("play.win", winner=winner), False)
await self.machine.raise_event_internal(Event("network_stop", None))
@@ -1938,7 +1984,9 @@ async def handle_client_disconnected(self, event: Event[str]) -> None:
error = event.data
print(f"[azul.game.PlayState] handle_client_disconnected {error = }")
- self.exit_data = (1, f"Client Disconnected$${error}", False)
+ client_disconnected = s_("error.client_disconnected")
+ error_text = s_(error)
+ self.exit_data = (1, f"{client_disconnected}$${error_text}", False)
async def do_actions(self) -> None:
"""Perform actions for this State."""
@@ -1969,7 +2017,7 @@ async def do_actions(self) -> None:
font,
visible=True,
color=Color(0, 0, 0),
- text=f"{message} - Return to Title",
+ text=s_("play.return_title_msg", message=message),
location=[x // 2 for x in SCREEN_SIZE],
handle_click=self.change_state("title"),
)
@@ -2141,7 +2189,7 @@ def screenshot_last_frame() -> None:
pygame.image.save(surface, fullpath, filename)
del surface
- print(f'Saved screenshot to "{fullpath}".')
+ print(s_("screenshot_save", fullpath=fullpath))
def cli_run() -> None:
@@ -2161,16 +2209,12 @@ def cli_run() -> None:
# Initialize Pygame
_success, fail = pygame.init()
if fail > 0:
- print(
- "Warning! Some modules of Pygame have not initialized properly!\n",
- "This can occur when not all required modules of SDL are installed.",
- )
+ print(s_("error.pygame_uninitialized"))
run()
except ExceptionGroup as exc:
## print(exc)
- ## exception = "".join(traceback.format_exception(exc))
+ exception = "".join(traceback.format_exception(exc))
## print(exception)
- traceback.print_exception(exc)
## raise
## except BaseException as ex:
## screenshot_last_frame()
diff --git a/src/azul/lang/en_us.json b/src/azul/lang/en_us.json
index 67ac85b..35b1356 100644
--- a/src/azul/lang/en_us.json
+++ b/src/azul/lang/en_us.json
@@ -1,24 +1,25 @@
{
- "main_menu": {
- "title": "Azul",
- "host_server": "Host Game",
- "join_server": "Join Game",
- "close": "Close"
- },
- "host_server_menu": {
- "title": "Host Server",
- "port_input": "Enter Host Port ({})",
- "start_server": "Start Server",
+ "connect": {
"return_title": "Return to Title"
},
- "join_server_menu": {
- "title": "Join Server",
- "host_input": "Enter Host Address",
- "port_input": "Enter Host Port ({})",
- "join_server": "Connect to Server",
- "return_title": "Return to Title"
+ "error": {
+ "client_disconnected": "Client Disconnected",
+ "not_connected": "Not connected to server.",
+ "pygame_uninitialized": "Warning! Some modules of Pygame have not initialized properly!\nThis can occur when not all required modules of SDL are installed.",
+ "read_event_fail": "Failed to read event from server.",
+ "socket_connect_fail": "Error connecting to server.",
+ "socket_eof": "Server closed connection."
+ },
+ "play": {
+ "return_title_msg": "{message} - Return to Title",
+ "win": "{winner} Won"
},
- "connect_server": {
- "connecting": "Connecting to Server..."
+ "screenshot_save": "Saved screenshot to \"{fullpath}\".",
+ "title": {
+ "game_title": "Azul",
+ "host_game": "Host Networked Game",
+ "join_game": "Join Networked Game",
+ "quit": "Quit",
+ "singleplayer": "Singleplayer Game"
}
}
diff --git a/src/azul/mr_floppy_test.py b/src/azul/mr_floppy_test.py
index 878df8c..3b55725 100644
--- a/src/azul/mr_floppy_test.py
+++ b/src/azul/mr_floppy_test.py
@@ -17,7 +17,7 @@
from pygame.locals import K_ESCAPE, KEYUP, QUIT, RESIZABLE, WINDOWRESIZED
from pygame.rect import Rect
-from azul import conf, lang, objects, sprite
+from azul import objects, sprite
from azul.statemachine import AsyncState, AsyncStateMachine
from azul.vector import Vector2
@@ -368,8 +368,6 @@ async def async_run() -> None:
"""Run client."""
global SCREEN_SIZE
# global client
- config = conf.load_config(path.join("conf", "main.conf"))
- lang.load_lang(config["Language"]["lang_name"])
screen = pygame.display.set_mode(
tuple(SCREEN_SIZE),
From 37495e43f5573edbdffe48017a4f1d8b79c6b0ec Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 11 Jan 2025 18:08:43 -0600
Subject: [PATCH 36/67] Update test-requirements
---
test-requirements.txt | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/test-requirements.txt b/test-requirements.txt
index dd75553..6e0a404 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -45,7 +45,7 @@ mypy-extensions==1.0.0
# mypy
numpy==2.1.3
# via -r test-requirements.in
-orjson==3.10.13 ; implementation_name == 'cpython'
+orjson==3.10.14
# via -r test-requirements.in
outcome==1.3.0.post0
# via
@@ -74,7 +74,7 @@ pytest-cov==6.0.0
# via -r test-requirements.in
pytest-trio==0.8.0
# via -r test-requirements.in
-ruff==0.8.4
+ruff==0.9.1
# via -r test-requirements.in
sniffio==1.3.1
# via trio
@@ -97,5 +97,5 @@ typing-extensions==4.12.2
# black
# libcomponent
# mypy
-uv==0.5.13
+uv==0.5.18
# via -r test-requirements.in
From ee541f950049be5c90a5946566f33c9283d25191 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 11 Jan 2025 18:21:15 -0600
Subject: [PATCH 37/67] Try fixing windows CI issue by explicity disabling
submodules
---
.github/workflows/ci.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 24e4a67..ac20006 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,6 +34,7 @@ jobs:
uses: actions/checkout@v4
with:
persist-credentials: false
+ submodules: false
- name: Setup python
uses: actions/setup-python@v5
with:
From 0e74738e6b8c3d4179382c06becf917c72aa284f Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 11 Jan 2025 18:36:37 -0600
Subject: [PATCH 38/67] Try using `set-safe-directory: false` to fix windows CI
issue Suggested from
https://github.com/actions/checkout/issues/1388#issuecomment-2229260270
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ac20006..9713dc8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,7 +34,7 @@ jobs:
uses: actions/checkout@v4
with:
persist-credentials: false
- submodules: false
+ set-safe-directory: false
- name: Setup python
uses: actions/setup-python@v5
with:
From 935ca82f98d8b96ed0237b78f0b95f3d39af1ff5 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 11 Jan 2025 18:47:50 -0600
Subject: [PATCH 39/67] Still trying to fix windows CI issues
---
.github/workflows/ci.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9713dc8..f94dac5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,11 +30,13 @@ jobs:
|| false
}}
steps:
+ - name: Print Git config
+ run: git config --list
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- set-safe-directory: false
+ submodules: false # Disable submodule fetching
- name: Setup python
uses: actions/setup-python@v5
with:
From bec254f9775225d0d5effd313a6f253178ce12c0 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 11 Jan 2025 18:54:50 -0600
Subject: [PATCH 40/67] Try not disabling `persist-credentials`
---
.github/workflows/ci.yml | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f94dac5..efa93a4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,12 +30,10 @@ jobs:
|| false
}}
steps:
- - name: Print Git config
- run: git config --list
- name: Checkout
uses: actions/checkout@v4
with:
- persist-credentials: false
+ persist-credentials: true # Set to `false` once git issue fixed
submodules: false # Disable submodule fetching
- name: Setup python
uses: actions/setup-python@v5
From 0eeda4f25336b5b23eb2460d3725e009cf0f52b3 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 11 Jan 2025 19:00:20 -0600
Subject: [PATCH 41/67] Rename files windows doesn't like
---
.github/workflows/ci.yml | 3 +--
src/azul/game.py | 2 +-
...24.png => Crash_at_Wed-Nov-13-15_30_42-2024.png} | Bin
...24.png => Crash_at_Wed_Nov_13_16_02_46_2024.png} | Bin
...24.png => Crash_at_Wed_Nov_13_16_20_10_2024.png} | Bin
...g => Screenshot_at_Sun-Jun-13-10_46_34-2021.png} | Bin
6 files changed, 2 insertions(+), 3 deletions(-)
rename src/azul/screenshots/{Crash_at_Wed-Nov-13-15:30:42-2024.png => Crash_at_Wed-Nov-13-15_30_42-2024.png} (100%)
rename src/azul/screenshots/{Crash_at_Wed_Nov_13_16:02:46_2024.png => Crash_at_Wed_Nov_13_16_02_46_2024.png} (100%)
rename src/azul/screenshots/{Crash_at_Wed_Nov_13_16:20:10_2024.png => Crash_at_Wed_Nov_13_16_20_10_2024.png} (100%)
rename src/azul/screenshots/{Screenshot_at_Sun-Jun-13-10:46:34-2021.png => Screenshot_at_Sun-Jun-13-10_46_34-2021.png} (100%)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index efa93a4..24e4a67 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,8 +33,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
with:
- persist-credentials: true # Set to `false` once git issue fixed
- submodules: false # Disable submodule fetching
+ persist-credentials: false
- name: Setup python
uses: actions/setup-python@v5
with:
diff --git a/src/azul/game.py b/src/azul/game.py
index 9d3140d..df3c1bb 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -2177,7 +2177,7 @@ def run() -> None:
def screenshot_last_frame() -> None:
"""Save the last frame before the game crashed."""
surface = pygame.display.get_surface().copy()
- str_time = "_".join(time.asctime().split(" "))
+ str_time = "_".join(time.asctime().split(" ")).replace(":", "_")
filename = f"Crash_at_{str_time}.png"
path = Path("screenshots").absolute()
diff --git a/src/azul/screenshots/Crash_at_Wed-Nov-13-15:30:42-2024.png b/src/azul/screenshots/Crash_at_Wed-Nov-13-15_30_42-2024.png
similarity index 100%
rename from src/azul/screenshots/Crash_at_Wed-Nov-13-15:30:42-2024.png
rename to src/azul/screenshots/Crash_at_Wed-Nov-13-15_30_42-2024.png
diff --git a/src/azul/screenshots/Crash_at_Wed_Nov_13_16:02:46_2024.png b/src/azul/screenshots/Crash_at_Wed_Nov_13_16_02_46_2024.png
similarity index 100%
rename from src/azul/screenshots/Crash_at_Wed_Nov_13_16:02:46_2024.png
rename to src/azul/screenshots/Crash_at_Wed_Nov_13_16_02_46_2024.png
diff --git a/src/azul/screenshots/Crash_at_Wed_Nov_13_16:20:10_2024.png b/src/azul/screenshots/Crash_at_Wed_Nov_13_16_20_10_2024.png
similarity index 100%
rename from src/azul/screenshots/Crash_at_Wed_Nov_13_16:20:10_2024.png
rename to src/azul/screenshots/Crash_at_Wed_Nov_13_16_20_10_2024.png
diff --git a/src/azul/screenshots/Screenshot_at_Sun-Jun-13-10:46:34-2021.png b/src/azul/screenshots/Screenshot_at_Sun-Jun-13-10_46_34-2021.png
similarity index 100%
rename from src/azul/screenshots/Screenshot_at_Sun-Jun-13-10:46:34-2021.png
rename to src/azul/screenshots/Screenshot_at_Sun-Jun-13-10_46_34-2021.png
From 190d68d8b9cd7a336a752274cdbfa3dd045ce5cd Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 18 Jan 2025 15:14:11 -0600
Subject: [PATCH 42/67] Mypy `--strict` already handles a lot
---
.pre-commit-config.yaml | 6 +++---
pyproject.toml | 19 +++----------------
2 files changed, 6 insertions(+), 19 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c8d04bc..6f7f3e3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -23,7 +23,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.8.4
+ rev: v0.9.2
hooks:
- id: ruff
types: [file]
@@ -39,11 +39,11 @@ repos:
additional_dependencies:
- tomli
- repo: https://github.com/crate-ci/typos
- rev: v1.28.4
+ rev: dictgen-v0.3.1
hooks:
- id: typos
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v0.10.0
+ rev: v1.2.0
hooks:
- id: zizmor
- repo: local
diff --git a/pyproject.toml b/pyproject.toml
index c790d1f..413c237 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -62,29 +62,16 @@ azul = ["py.typed", "data/*", "lang/*", "fonts/*"]
[tool.mypy]
plugins = ["numpy.typing.mypy_plugin"]
files = ["src/azul/", "computer_players"]
-check_untyped_defs = true
+show_column_numbers = true
+show_error_codes = true
+show_traceback = true
disallow_any_decorated = true
-disallow_any_generics = true
disallow_any_unimported = true
-disallow_incomplete_defs = true
-disallow_subclassing_any = true
-disallow_untyped_calls = true
-disallow_untyped_decorators = true
-disallow_untyped_defs = true
ignore_missing_imports = true
local_partial_types = true
no_implicit_optional = true
-no_implicit_reexport = true
-show_column_numbers = true
-show_error_codes = true
-show_traceback = true
strict = true
-strict_equality = true
-warn_redundant_casts = true
-warn_return_any = true
warn_unreachable = true
-warn_unused_configs = true
-warn_unused_ignores = true
[tool.ruff.lint.isort]
combine-as-imports = true
From 961b5182c7f1406df5450620f8615a91336494af Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 18 Jan 2025 17:55:31 -0600
Subject: [PATCH 43/67] Switch to uv-based lock file
---
.github/workflows/autodeps.yml | 16 +-
.github/workflows/ci.yml | 2 +
.pre-commit-config.yaml | 10 +-
check.sh | 15 +-
ci.sh | 44 +-
pyproject.toml | 24 +-
test-requirements.in | 24 --
test-requirements.txt | 101 -----
tests/test_async_clock.py | 2 +-
tools/project_requirements.py | 106 -----
uv.lock | 762 +++++++++++++++++++++++++++++++++
11 files changed, 836 insertions(+), 270 deletions(-)
delete mode 100644 test-requirements.in
delete mode 100644 test-requirements.txt
delete mode 100755 tools/project_requirements.py
create mode 100644 uv.lock
diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml
index 7a08a2b..a41f4ff 100644
--- a/.github/workflows/autodeps.yml
+++ b/.github/workflows/autodeps.yml
@@ -30,21 +30,17 @@ jobs:
- name: Bump dependencies
run: |
- python -m pip install -U pip pre-commit
- python -m pip install -r test-requirements.txt
- uv pip compile --universal --python-version=3.10 --upgrade test-requirements.in -o test-requirements.txt
- pre-commit autoupdate --jobs 0
+ python -m pip install -U uv
+ uv lock --upgrade
+ uv tool install pre-commit
+ uv run pre-commit autoupdate --jobs 0
- name: Install new requirements
- run: python -m pip install -r test-requirements.txt
+ run: uv sync
# apply newer versions' formatting
- name: Pre-commit updates
- run: pre-commit run -a
-
- - name: uv
- run: |
- uv pip compile --universal --python-version=3.10 test-requirements.in -o test-requirements.txt
+ run: uv run pre-commit run -a
- name: Commit changes and create automerge PR
env:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 24e4a67..b56f28d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,5 +1,7 @@
name: CI
+permissions: {}
+
on:
push:
branches-ignore:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6f7f3e3..e689c73 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -43,14 +43,6 @@ repos:
hooks:
- id: typos
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v1.2.0
+ rev: v1.2.1
hooks:
- id: zizmor
- - repo: local
- hooks:
- - id: project-requirements
- name: regenerate requirements.in
- language: python
- entry: tools/project_requirements.py
- pass_filenames: false
- files: ^(test-requirements.in)|(pyproject.toml)$
diff --git a/check.sh b/check.sh
index fb0c125..c382508 100755
--- a/check.sh
+++ b/check.sh
@@ -73,15 +73,15 @@ fi
# Check pip compile is consistent
echo "::group::Pip Compile - Tests"
-uv pip compile --universal --python-version=3.10 test-requirements.in -o test-requirements.txt
+uv lock
echo "::endgroup::"
-if git status --porcelain | grep -q "requirements.txt"; then
- echo "::error::requirements.txt changed."
- echo "::group::requirements.txt changed"
- echo "* requirements.txt changed" >> "$GITHUB_STEP_SUMMARY"
+if git status --porcelain | grep -q "uv.lock"; then
+ echo "::error::uv.lock changed."
+ echo "::group::uv.lock changed"
+ echo "* uv.lock changed" >> "$GITHUB_STEP_SUMMARY"
git status --porcelain
- git --no-pager diff --color ./*requirements.txt
+ git --no-pager diff --color ./*uv.lock
EXIT_STATUS=1
echo "::endgroup::"
fi
@@ -97,9 +97,10 @@ if [ $EXIT_STATUS -ne 0 ]; then
Problems were found by static analysis (listed above).
To fix formatting and see remaining errors, run
- uv pip install -r test-requirements.txt
+ uv sync --extra tools
black src/$PROJECT
ruff check src/$PROJECT
+ mypy
./check.sh
in your local checkout.
diff --git a/ci.sh b/ci.sh
index 81cc25a..6fb887f 100755
--- a/ci.sh
+++ b/ci.sh
@@ -19,18 +19,36 @@ python -c "import sys, struct; print('python:', sys.version); print('version_inf
echo "::endgroup::"
echo "::group::Install dependencies"
-python -m pip install -U pip uv -c test-requirements.txt
+python -m pip install -U pip tomli
python -m pip --version
+UV_VERSION=$(python -c 'import tomli; from pathlib import Path; print({p["name"]:p for p in tomli.loads(Path("uv.lock").read_text())["package"]}["uv"]["version"])')
+python -m pip install uv==$UV_VERSION
python -m uv --version
-python -m uv pip install build
+UV_VENV_SEED="pip"
+UV_VENV_OUTPUT="$(uv venv --seed --allow-existing 2>&1)"
+echo "$UV_VENV_OUTPUT"
-python -m build
-wheel_package=$(ls dist/*.whl)
-python -m uv pip install "$PROJECT @ $wheel_package" -c test-requirements.txt
+# Extract the activation command from the output
+activation_command=$(echo "$UV_VENV_OUTPUT" | grep -oP '(?<=Activate with: ).*')
+
+# Check if the activation command was found
+if [ -n "$activation_command" ]; then
+ # Execute the activation command
+ echo "Activating virtual environment..."
+ eval "$activation_command"
+else
+ echo "::error:: Activation command not found in uv venv output."
+ exit 1
+fi
+python -m pip install uv==$UV_VERSION
+
+# python -m uv build
+# wheel_package=$(ls dist/*.whl)
+# python -m uv pip install "$PROJECT @ $wheel_package" -c test-requirements.txt
if [ "$CHECK_FORMATTING" = "1" ]; then
- python -m uv pip install -r test-requirements.txt exceptiongroup
+ python -m uv sync --extra tests --extra tools
echo "::endgroup::"
source check.sh
else
@@ -38,10 +56,12 @@ else
# expands to 0 != 1 if NO_TEST_REQUIREMENTS is not set, if set the `-0` has no effect
# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02
if [ "${NO_TEST_REQUIREMENTS-0}" == 1 ]; then
- python -m uv pip install pytest coverage -c test-requirements.txt
- flags="--skip-optional-imports"
+ # python -m uv pip install pytest coverage -c test-requirements.txt
+ python -m uv sync --extra tests
+ flags=""
+ #"--skip-optional-imports"
else
- python -m uv pip install -r test-requirements.txt
+ python -m uv sync --extra tests --extra tools
flags=""
fi
@@ -71,10 +91,14 @@ else
else
PASSED=false
fi
+ PREV_DIR="$PWD"
+ cd "$INSTALLDIR"
+ rm pyproject.toml
+ cd "$PREV_DIR"
echo "::endgroup::"
echo "::group::Coverage"
- #coverage combine --rcfile ../pyproject.toml
+ coverage combine --rcfile ../pyproject.toml
coverage report -m --rcfile ../pyproject.toml
coverage xml --rcfile ../pyproject.toml
diff --git a/pyproject.toml b/pyproject.toml
index 413c237..ebdd41e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,7 +43,6 @@ dependencies = [
"orjson>=3.10,<4",
"pygame~=2.6.0",
"trio~=0.28.0",
- "typing_extensions>=4.12.2",
]
[tool.setuptools.dynamic]
@@ -53,12 +52,30 @@ version = {attr = "azul.game.__version__"}
"Source" = "https://github.com/CoolCat467/Azul"
"Bug Tracker" = "https://github.com/CoolCat467/Azul/issues"
-[project.scripts]
+[project.gui-scripts]
azul_game = "azul.game:cli_run"
+[project.optional-dependencies]
+tests = [
+ "pytest>=5.0",
+ "pytest-cov",
+ "pytest-trio",
+ "coverage>=7.2.5",
+ "uv>=0.5.21",
+ "mypy>=1.14.1",
+]
+tools = [
+ 'black>=24.10.0; implementation_name == "cpython"',
+ "ruff>=0.9.2",
+ "codespell>=2.3.0",
+]
+
[tool.setuptools.package-data]
azul = ["py.typed", "data/*", "lang/*", "fonts/*"]
+[tool.uv]
+package = true
+
[tool.mypy]
plugins = ["numpy.typing.mypy_plugin"]
files = ["src/azul/", "computer_players"]
@@ -152,6 +169,9 @@ source_pkgs = ["azul"]
omit = [
"__init__.py",
]
+parallel = true
+relative_files = true
+source = ["."]
[tool.coverage.report]
precision = 1
diff --git a/test-requirements.in b/test-requirements.in
deleted file mode 100644
index 105217b..0000000
--- a/test-requirements.in
+++ /dev/null
@@ -1,24 +0,0 @@
-# For tests
-pytest >= 5.0
-coverage >= 7.2.5
-pytest-trio
-pytest-cov
-
-# Tools
-black; implementation_name == "cpython"
-mypy >= 1.14.0
-ruff >= 0.6.6
-uv >= 0.2.24
-codespell
-
-# Azul's own dependencies
-#
-exceptiongroup; python_version < '3.11'
-libcomponent~=0.0.1
-mypy_extensions>=1.0.0
-numpy~=2.1.3
-orjson>=3.10,<4
-pygame~=2.6.0
-trio~=0.28.0
-typing_extensions>=4.12.2
-#
diff --git a/test-requirements.txt b/test-requirements.txt
deleted file mode 100644
index 6e0a404..0000000
--- a/test-requirements.txt
+++ /dev/null
@@ -1,101 +0,0 @@
-# This file was autogenerated by uv via the following command:
-# uv pip compile --universal --python-version=3.10 test-requirements.in -o test-requirements.txt
-attrs==24.3.0
- # via
- # outcome
- # trio
-black==24.10.0 ; implementation_name == 'cpython'
- # via -r test-requirements.in
-cffi==1.17.1 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_python_implementation != 'PyPy'
- # via
- # cryptography
- # trio
-click==8.1.8 ; implementation_name == 'cpython'
- # via black
-codespell==2.3.0
- # via -r test-requirements.in
-colorama==0.4.6 ; sys_platform == 'win32'
- # via
- # click
- # pytest
-coverage==7.6.10
- # via
- # -r test-requirements.in
- # pytest-cov
-cryptography==44.0.0
- # via libcomponent
-exceptiongroup==1.2.2 ; python_full_version < '3.11'
- # via
- # -r test-requirements.in
- # pytest
- # trio
-idna==3.10
- # via trio
-iniconfig==2.0.0
- # via pytest
-libcomponent==0.0.1
- # via -r test-requirements.in
-mypy==1.14.1
- # via -r test-requirements.in
-mypy-extensions==1.0.0
- # via
- # -r test-requirements.in
- # black
- # libcomponent
- # mypy
-numpy==2.1.3
- # via -r test-requirements.in
-orjson==3.10.14
- # via -r test-requirements.in
-outcome==1.3.0.post0
- # via
- # pytest-trio
- # trio
-packaging==24.2
- # via
- # black
- # pytest
-pathspec==0.12.1 ; implementation_name == 'cpython'
- # via black
-platformdirs==4.3.6 ; implementation_name == 'cpython'
- # via black
-pluggy==1.5.0
- # via pytest
-pycparser==2.22 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_python_implementation != 'PyPy'
- # via cffi
-pygame==2.6.1
- # via -r test-requirements.in
-pytest==8.3.4
- # via
- # -r test-requirements.in
- # pytest-cov
- # pytest-trio
-pytest-cov==6.0.0
- # via -r test-requirements.in
-pytest-trio==0.8.0
- # via -r test-requirements.in
-ruff==0.9.1
- # via -r test-requirements.in
-sniffio==1.3.1
- # via trio
-sortedcontainers==2.4.0
- # via trio
-tomli==2.2.1 ; python_full_version <= '3.11'
- # via
- # black
- # coverage
- # mypy
- # pytest
-trio==0.28.0
- # via
- # -r test-requirements.in
- # libcomponent
- # pytest-trio
-typing-extensions==4.12.2
- # via
- # -r test-requirements.in
- # black
- # libcomponent
- # mypy
-uv==0.5.18
- # via -r test-requirements.in
diff --git a/tests/test_async_clock.py b/tests/test_async_clock.py
index 4297787..fa8256d 100644
--- a/tests/test_async_clock.py
+++ b/tests/test_async_clock.py
@@ -56,7 +56,7 @@ async def test_tick(clock: Clock) -> None:
@pytest.mark.trio
async def test_tick_fps(clock: Clock) -> None:
for _ in range(20):
- await clock.tick(60)
+ await clock.tick(1024)
fps = clock.get_fps()
assert isinstance(fps, float)
assert fps >= 0
diff --git a/tools/project_requirements.py b/tools/project_requirements.py
deleted file mode 100755
index edcfbad..0000000
--- a/tools/project_requirements.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python3
-
-"""Project Requirements - Write test-requirements.in based on pyproject.toml."""
-
-# Programmed by CoolCat467
-
-from __future__ import annotations
-
-# Project Requirements - Write test-requirements.in based on pyproject.toml.
-# Copyright (C) 2024 CoolCat467
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-__title__ = "Project Requirements"
-__author__ = "CoolCat467"
-__version__ = "0.0.0"
-__license__ = "GNU General Public License Version 3"
-
-import sys
-from pathlib import Path
-from typing import Final
-
-import tomllib
-
-# Key to start replacing inside of contents
-KEY: Final = "TOML_DEPENDENCIES"
-
-
-def run() -> None:
- """Run program."""
- # Find root folder
- this = Path(__file__).absolute()
- tools = this.parent
- root = tools.parent
- # Make sure it's right
- assert (root / "LICENSE").exists(), "Not in correct directory!"
-
- # Read pyproject.toml
- pyproject = root / "pyproject.toml"
- with pyproject.open("rb") as fp:
- data = tomllib.load(fp)
-
- # Get dependencies list
- assert isinstance(data, dict)
- project = data["project"]
- assert isinstance(project, dict)
- dependencies = project["dependencies"]
- assert isinstance(dependencies, list)
-
- # Read requirements file
- requirements_list = root / "test-requirements.in"
- assert requirements_list.exists(), f"{requirements_list} does not exist!"
- requirements_data = requirements_list.read_text("utf-8")
-
- # Find out what start and end should be based on key.
- key_start = f"<{KEY}>"
- key_end = f"{KEY}>"
-
- # Try to find start and end triggers in requirements data
- start_char = requirements_data.find(key_start)
- end_char = requirements_data.find(key_end)
- if -1 in {start_char, end_char}:
- raise ValueError(
- f"{key_start!r} or {key_end!r} not found in {requirements_list}",
- )
-
- # Create overwrite text
- dependencies_text = "\n".join(sorted(dependencies))
- overwrite_text = "\n".join(
- (
- key_start,
- dependencies_text,
- f"#{key_end}",
- ),
- )
- # Create new file contents
- end = end_char + len(key_end)
- new_text = (
- requirements_data[:start_char]
- + overwrite_text
- + requirements_data[end:]
- )
-
- # If new text differs, overwrite and alert
- if new_text != requirements_data:
- print("Requirements file is outdated...")
- requirements_list.write_text(new_text, "utf-8")
- print("Requirements file updated successfully.")
- return 1
- print("Requirements file is up to date.")
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(run())
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..a9fa758
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,762 @@
+version = 1
+requires-python = ">=3.10"
+
+[[package]]
+name = "attrs"
+version = "24.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 },
+]
+
+[[package]]
+name = "azul"
+source = { editable = "." }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "libcomponent" },
+ { name = "mypy-extensions" },
+ { name = "numpy" },
+ { name = "orjson" },
+ { name = "pygame" },
+ { name = "trio" },
+]
+
+[package.optional-dependencies]
+tests = [
+ { name = "coverage" },
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "pytest-trio" },
+ { name = "uv" },
+]
+tools = [
+ { name = "black", marker = "implementation_name == 'cpython'" },
+ { name = "codespell" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "black", marker = "implementation_name == 'cpython' and extra == 'tools'", specifier = ">=24.10.0" },
+ { name = "codespell", marker = "extra == 'tools'", specifier = ">=2.3.0" },
+ { name = "coverage", marker = "extra == 'tests'", specifier = ">=7.2.5" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "libcomponent", specifier = "~=0.0.1" },
+ { name = "mypy", marker = "extra == 'tests'", specifier = ">=1.14.1" },
+ { name = "mypy-extensions", specifier = ">=1.0.0" },
+ { name = "numpy", specifier = "~=2.1.3" },
+ { name = "orjson", specifier = ">=3.10,<4" },
+ { name = "pygame", specifier = "~=2.6.0" },
+ { name = "pytest", marker = "extra == 'tests'", specifier = ">=5.0" },
+ { name = "pytest-cov", marker = "extra == 'tests'" },
+ { name = "pytest-trio", marker = "extra == 'tests'" },
+ { name = "ruff", marker = "extra == 'tools'", specifier = ">=0.9.2" },
+ { name = "trio", specifier = "~=0.28.0" },
+ { name = "uv", marker = "extra == 'tests'", specifier = ">=0.5.21" },
+]
+
+[[package]]
+name = "black"
+version = "24.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "mypy-extensions" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "platformdirs" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/f3/465c0eb5cddf7dbbfe1fecd9b875d1dcf51b88923cd2c1d7e9ab95c6336b/black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", size = 1623211 },
+ { url = "https://files.pythonhosted.org/packages/df/57/b6d2da7d200773fdfcc224ffb87052cf283cec4d7102fab450b4a05996d8/black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", size = 1457139 },
+ { url = "https://files.pythonhosted.org/packages/6e/c5/9023b7673904a5188f9be81f5e129fff69f51f5515655fbd1d5a4e80a47b/black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", size = 1753774 },
+ { url = "https://files.pythonhosted.org/packages/e1/32/df7f18bd0e724e0d9748829765455d6643ec847b3f87e77456fc99d0edab/black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e", size = 1414209 },
+ { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468 },
+ { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270 },
+ { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061 },
+ { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293 },
+ { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256 },
+ { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534 },
+ { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892 },
+ { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796 },
+ { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986 },
+ { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085 },
+ { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928 },
+ { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875 },
+ { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898 },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 },
+ { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 },
+ { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 },
+ { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 },
+ { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 },
+ { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 },
+ { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 },
+ { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 },
+ { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 },
+ { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 },
+ { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 },
+ { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 },
+ { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 },
+ { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 },
+ { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
+ { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
+ { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
+ { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
+ { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
+ { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
+ { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
+ { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
+ { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
+ { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 },
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
+]
+
+[[package]]
+name = "codespell"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a0/a9/98353dfc7afcdf18cffd2dd3e959a25eaaf2728cf450caa59af89648a8e4/codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f", size = 329791 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/20/b6019add11e84f821184234cea0ad91442373489ef7ccfa3d73a71b908fa/codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1", size = 329167 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "coverage"
+version = "7.6.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/12/2a2a923edf4ddabdffed7ad6da50d96a5c126dae7b80a33df7310e329a1e/coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", size = 207982 },
+ { url = "https://files.pythonhosted.org/packages/ca/49/6985dbca9c7be3f3cb62a2e6e492a0c88b65bf40579e16c71ae9c33c6b23/coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", size = 208414 },
+ { url = "https://files.pythonhosted.org/packages/35/93/287e8f1d1ed2646f4e0b2605d14616c9a8a2697d0d1b453815eb5c6cebdb/coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", size = 236860 },
+ { url = "https://files.pythonhosted.org/packages/de/e1/cfdb5627a03567a10031acc629b75d45a4ca1616e54f7133ca1fa366050a/coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", size = 234758 },
+ { url = "https://files.pythonhosted.org/packages/6d/85/fc0de2bcda3f97c2ee9fe8568f7d48f7279e91068958e5b2cc19e0e5f600/coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", size = 235920 },
+ { url = "https://files.pythonhosted.org/packages/79/73/ef4ea0105531506a6f4cf4ba571a214b14a884630b567ed65b3d9c1975e1/coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", size = 234986 },
+ { url = "https://files.pythonhosted.org/packages/c6/4d/75afcfe4432e2ad0405c6f27adeb109ff8976c5e636af8604f94f29fa3fc/coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", size = 233446 },
+ { url = "https://files.pythonhosted.org/packages/86/5b/efee56a89c16171288cafff022e8af44f8f94075c2d8da563c3935212871/coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", size = 234566 },
+ { url = "https://files.pythonhosted.org/packages/f2/db/67770cceb4a64d3198bf2aa49946f411b85ec6b0a9b489e61c8467a4253b/coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", size = 210675 },
+ { url = "https://files.pythonhosted.org/packages/8d/27/e8bfc43f5345ec2c27bc8a1fa77cdc5ce9dcf954445e11f14bb70b889d14/coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", size = 211518 },
+ { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 },
+ { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 },
+ { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 },
+ { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 },
+ { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 },
+ { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 },
+ { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 },
+ { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 },
+ { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 },
+ { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 },
+ { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 },
+ { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 },
+ { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 },
+ { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 },
+ { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 },
+ { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 },
+ { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 },
+ { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 },
+ { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 },
+ { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 },
+ { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 },
+ { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 },
+ { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 },
+ { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 },
+ { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 },
+ { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 },
+ { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 },
+ { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 },
+ { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 },
+ { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 },
+ { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 },
+ { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 },
+ { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 },
+ { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 },
+ { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 },
+ { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 },
+ { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 },
+ { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 },
+ { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 },
+ { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 },
+ { url = "https://files.pythonhosted.org/packages/a1/70/de81bfec9ed38a64fc44a77c7665e20ca507fc3265597c28b0d989e4082e/coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f", size = 200223 },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "cryptography"
+version = "44.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 },
+ { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 },
+ { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 },
+ { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 },
+ { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 },
+ { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 },
+ { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 },
+ { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 },
+ { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 },
+ { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 },
+ { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 },
+ { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 },
+ { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 },
+ { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 },
+ { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 },
+ { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 },
+ { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 },
+ { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 },
+ { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 },
+ { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 },
+ { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857 },
+ { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615 },
+ { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622 },
+ { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546 },
+ { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937 },
+ { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
+]
+
+[[package]]
+name = "libcomponent"
+version = "0.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+ { name = "mypy-extensions" },
+ { name = "trio" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/40/30e51ad19bc3420a61e60c300c2bc4a9f685ba587bdcf8e852dd5a3a9a40/libcomponent-0.0.1.tar.gz", hash = "sha256:fdebdc9b4857707511c3b2bf0a434af30910b19fa0ec1ddb3e78457d074e40f1", size = 74693 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/2f/404652eae9da1f1216c66e57d016debaec8ec927fcd2a2c3c600629ceb25/libcomponent-0.0.1-py3-none-any.whl", hash = "sha256:be0fe9fffcebe4602656d55fb1c1d8afa230552d84cf1d69300b23d44e19369a", size = 57644 },
+]
+
+[[package]]
+name = "mypy"
+version = "1.14.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mypy-extensions" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002 },
+ { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400 },
+ { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172 },
+ { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732 },
+ { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197 },
+ { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836 },
+ { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 },
+ { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 },
+ { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 },
+ { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 },
+ { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 },
+ { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 },
+ { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 },
+ { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 },
+ { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 },
+ { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 },
+ { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 },
+ { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 },
+ { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 },
+ { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 },
+ { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 },
+ { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 },
+ { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 },
+ { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 },
+ { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
+]
+
+[[package]]
+name = "numpy"
+version = "2.1.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/80/d572a4737626372915bca41c3afbfec9d173561a39a0a61bacbbfd1dafd4/numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", size = 21152472 },
+ { url = "https://files.pythonhosted.org/packages/6f/bb/7bfba10c791ae3bb6716da77ad85a82d5fac07fc96fb0023ef0571df9d20/numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", size = 13747967 },
+ { url = "https://files.pythonhosted.org/packages/da/d6/2df7bde35f0478455f0be5934877b3e5a505f587b00230f54a519a6b55a5/numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", size = 5354921 },
+ { url = "https://files.pythonhosted.org/packages/d1/bb/75b945874f931494891eac6ca06a1764d0e8208791f3addadb2963b83527/numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", size = 6888603 },
+ { url = "https://files.pythonhosted.org/packages/68/a7/fde73636f6498dbfa6d82fc336164635fe592f1ad0d13285fcb6267fdc1c/numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", size = 13889862 },
+ { url = "https://files.pythonhosted.org/packages/05/db/5d9c91b2e1e2e72be1369278f696356d44975befcae830daf2e667dcb54f/numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", size = 16328151 },
+ { url = "https://files.pythonhosted.org/packages/3e/6a/7eb732109b53ae64a29e25d7e68eb9d6611037f6354875497008a49e74d3/numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", size = 16704107 },
+ { url = "https://files.pythonhosted.org/packages/88/cc/278113b66a1141053cbda6f80e4200c6da06b3079c2d27bda1fde41f2c1f/numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4", size = 14385789 },
+ { url = "https://files.pythonhosted.org/packages/f5/69/eb20f5e1bfa07449bc67574d2f0f7c1e6b335fb41672e43861a7727d85f2/numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", size = 6536706 },
+ { url = "https://files.pythonhosted.org/packages/8e/8b/1c131ab5a94c1086c289c6e1da1d843de9dbd95fe5f5ee6e61904c9518e2/numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", size = 12864165 },
+ { url = "https://files.pythonhosted.org/packages/ad/81/c8167192eba5247593cd9d305ac236847c2912ff39e11402e72ae28a4985/numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", size = 21156252 },
+ { url = "https://files.pythonhosted.org/packages/da/74/5a60003fc3d8a718d830b08b654d0eea2d2db0806bab8f3c2aca7e18e010/numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", size = 13784119 },
+ { url = "https://files.pythonhosted.org/packages/47/7c/864cb966b96fce5e63fcf25e1e4d957fe5725a635e5f11fe03f39dd9d6b5/numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", size = 5352978 },
+ { url = "https://files.pythonhosted.org/packages/09/ac/61d07930a4993dd9691a6432de16d93bbe6aa4b1c12a5e573d468eefc1ca/numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", size = 6892570 },
+ { url = "https://files.pythonhosted.org/packages/27/2f/21b94664f23af2bb52030653697c685022119e0dc93d6097c3cb45bce5f9/numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", size = 13896715 },
+ { url = "https://files.pythonhosted.org/packages/7a/f0/80811e836484262b236c684a75dfc4ba0424bc670e765afaa911468d9f39/numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", size = 16339644 },
+ { url = "https://files.pythonhosted.org/packages/fa/81/ce213159a1ed8eb7d88a2a6ef4fbdb9e4ffd0c76b866c350eb4e3c37e640/numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", size = 16712217 },
+ { url = "https://files.pythonhosted.org/packages/7d/84/4de0b87d5a72f45556b2a8ee9fc8801e8518ec867fc68260c1f5dcb3903f/numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", size = 14399053 },
+ { url = "https://files.pythonhosted.org/packages/7e/1c/e5fabb9ad849f9d798b44458fd12a318d27592d4bc1448e269dec070ff04/numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", size = 6534741 },
+ { url = "https://files.pythonhosted.org/packages/1e/48/a9a4b538e28f854bfb62e1dea3c8fea12e90216a276c7777ae5345ff29a7/numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", size = 12869487 },
+ { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658 },
+ { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258 },
+ { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249 },
+ { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704 },
+ { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089 },
+ { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185 },
+ { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751 },
+ { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705 },
+ { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077 },
+ { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858 },
+ { url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263 },
+ { url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771 },
+ { url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805 },
+ { url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380 },
+ { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451 },
+ { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822 },
+ { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822 },
+ { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598 },
+ { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021 },
+ { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405 },
+ { url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062 },
+ { url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839 },
+ { url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031 },
+ { url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977 },
+ { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951 },
+ { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655 },
+ { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902 },
+ { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180 },
+ { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907 },
+ { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 },
+ { url = "https://files.pythonhosted.org/packages/00/e7/8d8bb791b62586cc432ecbb70632b4f23b7b7c88df41878de7528264f6d7/numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", size = 20983893 },
+ { url = "https://files.pythonhosted.org/packages/5e/f3/cb8118a044b5007586245a650360c9f5915b2f4232dd7658bb7a63dd1d02/numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", size = 6752501 },
+ { url = "https://files.pythonhosted.org/packages/53/f5/365b46439b518d2ec6ebb880cc0edf90f225145dfd4db7958334f7164530/numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", size = 16142601 },
+ { url = "https://files.pythonhosted.org/packages/03/c2/d1fee6ba999aa7cd41ca6856937f2baaf604c3eec1565eae63451ec31e5e/numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", size = 12771397 },
+]
+
+[[package]]
+name = "orjson"
+version = "3.10.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/09/e5ff18ad009e6f97eb7edc5f67ef98b3ce0c189da9c3eaca1f9587cd4c61/orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04", size = 249532 },
+ { url = "https://files.pythonhosted.org/packages/bd/b8/a75883301fe332bd433d9b0ded7d2bb706ccac679602c3516984f8814fb5/orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8", size = 125229 },
+ { url = "https://files.pythonhosted.org/packages/83/4b/22f053e7a364cc9c685be203b1e40fc5f2b3f164a9b2284547504eec682e/orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8", size = 150148 },
+ { url = "https://files.pythonhosted.org/packages/63/64/1b54fc75ca328b57dd810541a4035fe48c12a161d466e3cf5b11a8c25649/orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814", size = 139748 },
+ { url = "https://files.pythonhosted.org/packages/5e/ff/ff0c5da781807bb0a5acd789d9a7fbcb57f7b0c6e1916595da1f5ce69f3c/orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164", size = 154559 },
+ { url = "https://files.pythonhosted.org/packages/4e/9a/11e2974383384ace8495810d4a2ebef5f55aacfc97b333b65e789c9d362d/orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf", size = 130349 },
+ { url = "https://files.pythonhosted.org/packages/2d/c4/dd9583aea6aefee1b64d3aed13f51d2aadb014028bc929fe52936ec5091f/orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061", size = 138514 },
+ { url = "https://files.pythonhosted.org/packages/53/3e/dcf1729230654f5c5594fc752de1f43dcf67e055ac0d300c8cdb1309269a/orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3", size = 130940 },
+ { url = "https://files.pythonhosted.org/packages/e8/2b/b9759fe704789937705c8a56a03f6c03e50dff7df87d65cba9a20fec5282/orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d", size = 414713 },
+ { url = "https://files.pythonhosted.org/packages/a7/6b/b9dfdbd4b6e20a59238319eb203ae07c3f6abf07eef909169b7a37ae3bba/orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182", size = 141028 },
+ { url = "https://files.pythonhosted.org/packages/7c/b5/40f5bbea619c7caf75eb4d652a9821875a8ed04acc45fe3d3ef054ca69fb/orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e", size = 129715 },
+ { url = "https://files.pythonhosted.org/packages/38/60/2272514061cbdf4d672edbca6e59c7e01cd1c706e881427d88f3c3e79761/orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab", size = 142473 },
+ { url = "https://files.pythonhosted.org/packages/11/5d/be1490ff7eafe7fef890eb4527cf5bcd8cfd6117f3efe42a3249ec847b60/orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806", size = 133564 },
+ { url = "https://files.pythonhosted.org/packages/7a/a2/21b25ce4a2c71dbb90948ee81bd7a42b4fbfc63162e57faf83157d5540ae/orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6", size = 249533 },
+ { url = "https://files.pythonhosted.org/packages/b2/85/2076fc12d8225698a51278009726750c9c65c846eda741e77e1761cfef33/orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef", size = 125230 },
+ { url = "https://files.pythonhosted.org/packages/06/df/a85a7955f11274191eccf559e8481b2be74a7c6d43075d0a9506aa80284d/orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334", size = 150148 },
+ { url = "https://files.pythonhosted.org/packages/37/b3/94c55625a29b8767c0eed194cb000b3787e3c23b4cdd13be17bae6ccbb4b/orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d", size = 139749 },
+ { url = "https://files.pythonhosted.org/packages/53/ba/c608b1e719971e8ddac2379f290404c2e914cf8e976369bae3cad88768b1/orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0", size = 154558 },
+ { url = "https://files.pythonhosted.org/packages/b2/c4/c1fb835bb23ad788a39aa9ebb8821d51b1c03588d9a9e4ca7de5b354fdd5/orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13", size = 130349 },
+ { url = "https://files.pythonhosted.org/packages/78/14/bb2b48b26ab3c570b284eb2157d98c1ef331a8397f6c8bd983b270467f5c/orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5", size = 138513 },
+ { url = "https://files.pythonhosted.org/packages/4a/97/d5b353a5fe532e92c46467aa37e637f81af8468aa894cd77d2ec8a12f99e/orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b", size = 130942 },
+ { url = "https://files.pythonhosted.org/packages/b5/5d/a067bec55293cca48fea8b9928cfa84c623be0cce8141d47690e64a6ca12/orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399", size = 414717 },
+ { url = "https://files.pythonhosted.org/packages/6f/9a/1485b8b05c6b4c4db172c438cf5db5dcfd10e72a9bc23c151a1137e763e0/orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388", size = 141033 },
+ { url = "https://files.pythonhosted.org/packages/f8/d2/fc67523656e43a0c7eaeae9007c8b02e86076b15d591e9be11554d3d3138/orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c", size = 129720 },
+ { url = "https://files.pythonhosted.org/packages/79/42/f58c7bd4e5b54da2ce2ef0331a39ccbbaa7699b7f70206fbf06737c9ed7d/orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e", size = 142473 },
+ { url = "https://files.pythonhosted.org/packages/00/f8/bb60a4644287a544ec81df1699d5b965776bc9848d9029d9f9b3402ac8bb/orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e", size = 133570 },
+ { url = "https://files.pythonhosted.org/packages/66/85/22fe737188905a71afcc4bf7cc4c79cd7f5bbe9ed1fe0aac4ce4c33edc30/orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a", size = 249504 },
+ { url = "https://files.pythonhosted.org/packages/48/b7/2622b29f3afebe938a0a9037e184660379797d5fd5234e5998345d7a5b43/orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d", size = 125080 },
+ { url = "https://files.pythonhosted.org/packages/ce/8f/0b72a48f4403d0b88b2a41450c535b3e8989e8a2d7800659a967efc7c115/orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0", size = 150121 },
+ { url = "https://files.pythonhosted.org/packages/06/ec/acb1a20cd49edb2000be5a0404cd43e3c8aad219f376ac8c60b870518c03/orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4", size = 139796 },
+ { url = "https://files.pythonhosted.org/packages/33/e1/f7840a2ea852114b23a52a1c0b2bea0a1ea22236efbcdb876402d799c423/orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767", size = 154636 },
+ { url = "https://files.pythonhosted.org/packages/fa/da/31543337febd043b8fa80a3b67de627669b88c7b128d9ad4cc2ece005b7a/orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41", size = 130621 },
+ { url = "https://files.pythonhosted.org/packages/ed/78/66115dc9afbc22496530d2139f2f4455698be444c7c2475cb48f657cefc9/orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514", size = 138516 },
+ { url = "https://files.pythonhosted.org/packages/22/84/cd4f5fb5427ffcf823140957a47503076184cb1ce15bcc1165125c26c46c/orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17", size = 130762 },
+ { url = "https://files.pythonhosted.org/packages/93/1f/67596b711ba9f56dd75d73b60089c5c92057f1130bb3a25a0f53fb9a583b/orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b", size = 414700 },
+ { url = "https://files.pythonhosted.org/packages/7c/0c/6a3b3271b46443d90efb713c3e4fe83fa8cd71cda0d11a0f69a03f437c6e/orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7", size = 141077 },
+ { url = "https://files.pythonhosted.org/packages/3b/9b/33c58e0bfc788995eccd0d525ecd6b84b40d7ed182dd0751cd4c1322ac62/orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a", size = 129898 },
+ { url = "https://files.pythonhosted.org/packages/01/c1/d577ecd2e9fa393366a1ea0a9267f6510d86e6c4bb1cdfb9877104cac44c/orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665", size = 142566 },
+ { url = "https://files.pythonhosted.org/packages/ed/eb/a85317ee1732d1034b92d56f89f1de4d7bf7904f5c8fb9dcdd5b1c83917f/orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa", size = 133732 },
+ { url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399 },
+ { url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044 },
+ { url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066 },
+ { url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737 },
+ { url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804 },
+ { url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583 },
+ { url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465 },
+ { url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742 },
+ { url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669 },
+ { url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043 },
+ { url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826 },
+ { url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542 },
+ { url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444 },
+]
+
+[[package]]
+name = "outcome"
+version = "1.3.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 },
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
+]
+
+[[package]]
+name = "pygame"
+version = "2.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", hash = "sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f", size = 14808125 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/0b/334c7c50a2979e15f2a027a41d1ca78ee730d5b1c7f7f4b26d7cb899839d/pygame-2.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9beeb647e555afb5657111fa83acb74b99ad88761108eaea66472e8b8547b55b", size = 13109297 },
+ { url = "https://files.pythonhosted.org/packages/dc/48/f8b1069788d1bd42e63a960d74d3355242480b750173a42b2749687578ca/pygame-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10e3d2a55f001f6c0a6eb44aa79ea7607091c9352b946692acedb2ac1482f1c9", size = 12375837 },
+ { url = "https://files.pythonhosted.org/packages/bc/33/a1310386b8913ce1bdb90c33fa536970e299ad57eb35785f1d71ea1e2ad3/pygame-2.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816e85000c5d8b02a42b9834f761a5925ef3377d2924e3a7c4c143d2990ce5b8", size = 13607860 },
+ { url = "https://files.pythonhosted.org/packages/88/0f/4e37b115056e43714e7550054dd3cd7f4d552da54d7fc58a2fb1407acda5/pygame-2.6.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a78fd030d98faab4a8e27878536fdff7518d3e062a72761c552f624ebba5a5f", size = 14304696 },
+ { url = "https://files.pythonhosted.org/packages/11/b3/de6ed93ae483cf3bac8f950a955e83f7ffe59651fd804d100fff65d66d6c/pygame-2.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da3ad64d685f84a34ebe5daacb39fff14f1251acb34c098d760d63fee768f50c", size = 13977684 },
+ { url = "https://files.pythonhosted.org/packages/d3/05/d86440aa879708c41844bafc6b3eb42c6d8cf54082482499b53139133e2a/pygame-2.6.1-cp310-cp310-win32.whl", hash = "sha256:9dd5c054d4bd875a8caf978b82672f02bec332f52a833a76899220c460bb4b58", size = 10251775 },
+ { url = "https://files.pythonhosted.org/packages/38/88/8de61324775cf2c844a51d8db14a8a6d2a9092312f27678f6eaa3a460376/pygame-2.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:00827aba089355925902d533f9c41e79a799641f03746c50a374dc5c3362e43d", size = 10618801 },
+ { url = "https://files.pythonhosted.org/packages/c4/ca/8f367cb9fe734c4f6f6400e045593beea2635cd736158f9fabf58ee14e3c/pygame-2.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:20349195326a5e82a16e351ed93465a7845a7e2a9af55b7bc1b2110ea3e344e1", size = 13113753 },
+ { url = "https://files.pythonhosted.org/packages/83/47/6edf2f890139616b3219be9cfcc8f0cb8f42eb15efd59597927e390538cb/pygame-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3935459109da4bb0b3901da9904f0a3e52028a3332a355d298b1673a334cf21", size = 12378146 },
+ { url = "https://files.pythonhosted.org/packages/00/9e/0d8aa8cf93db2d2ee38ebaf1c7b61d0df36ded27eb726221719c150c673d/pygame-2.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31dbdb5d0217f32764797d21c2752e258e5fb7e895326538d82b5f75a0cd856", size = 13611760 },
+ { url = "https://files.pythonhosted.org/packages/d7/9e/d06adaa5cc65876bcd7a24f59f67e07f7e4194e6298130024ed3fb22c456/pygame-2.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:173badf82fa198e6888017bea40f511cb28e69ecdd5a72b214e81e4dcd66c3b1", size = 14298054 },
+ { url = "https://files.pythonhosted.org/packages/7a/a1/9ae2852ebd3a7cc7d9ae7ff7919ab983e4a5c1b7a14e840732f23b2b48f6/pygame-2.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce8cc108b92de9b149b344ad2e25eedbe773af0dc41dfb24d1f07f679b558c60", size = 13977107 },
+ { url = "https://files.pythonhosted.org/packages/31/df/6788fd2e9a864d0496a77670e44a7c012184b7a5382866ab0e60c55c0f28/pygame-2.6.1-cp311-cp311-win32.whl", hash = "sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c", size = 10250863 },
+ { url = "https://files.pythonhosted.org/packages/d2/55/ca3eb851aeef4f6f2e98a360c201f0d00bd1ba2eb98e2c7850d80aabc526/pygame-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299", size = 10622016 },
+ { url = "https://files.pythonhosted.org/packages/92/16/2c602c332f45ff9526d61f6bd764db5096ff9035433e2172e2d2cadae8db/pygame-2.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ee7f2771f588c966fa2fa8b829be26698c9b4836f82ede5e4edc1a68594942e", size = 13118279 },
+ { url = "https://files.pythonhosted.org/packages/cd/53/77ccbc384b251c6e34bfd2e734c638233922449a7844e3c7a11ef91cee39/pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8040ea2ab18c6b255af706ec01355c8a6b08dc48d77fd4ee783f8fc46a843bf", size = 12384524 },
+ { url = "https://files.pythonhosted.org/packages/06/be/3ed337583f010696c3b3435e89a74fb29d0c74d0931e8f33c0a4246307a9/pygame-2.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47a6938de93fa610accd4969e638c2aebcb29b2fca518a84c3a39d91ab47116", size = 13587123 },
+ { url = "https://files.pythonhosted.org/packages/fd/ca/b015586a450db59313535662991b34d24c1f0c0dc149cc5f496573900f4e/pygame-2.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33006f784e1c7d7e466fcb61d5489da59cc5f7eb098712f792a225df1d4e229d", size = 14275532 },
+ { url = "https://files.pythonhosted.org/packages/b9/f2/d31e6ad42d657af07be2ffd779190353f759a07b51232b9e1d724f2cda46/pygame-2.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1206125f14cae22c44565c9d333607f1d9f59487b1f1432945dfc809aeaa3e88", size = 13952653 },
+ { url = "https://files.pythonhosted.org/packages/f3/42/8ea2a6979e6fa971702fece1747e862e2256d4a8558fe0da6364dd946c53/pygame-2.6.1-cp312-cp312-win32.whl", hash = "sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e", size = 10252421 },
+ { url = "https://files.pythonhosted.org/packages/5f/90/7d766d54bb95939725e9a9361f9c06b0cfbe3fe100aa35400f0a461a278a/pygame-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65", size = 10624591 },
+ { url = "https://files.pythonhosted.org/packages/e1/91/718acf3e2a9d08a6ddcc96bd02a6f63c99ee7ba14afeaff2a51c987df0b9/pygame-2.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2", size = 13090765 },
+ { url = "https://files.pythonhosted.org/packages/0e/c6/9cb315de851a7682d9c7568a41ea042ee98d668cb8deadc1dafcab6116f0/pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171", size = 12381704 },
+ { url = "https://files.pythonhosted.org/packages/9f/8f/617a1196e31ae3b46be6949fbaa95b8c93ce15e0544266198c2266cc1b4d/pygame-2.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b", size = 13581091 },
+ { url = "https://files.pythonhosted.org/packages/3b/87/2851a564e40a2dad353f1c6e143465d445dab18a95281f9ea458b94f3608/pygame-2.6.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b", size = 14273844 },
+ { url = "https://files.pythonhosted.org/packages/85/b5/aa23aa2e70bcba42c989c02e7228273c30f3b44b9b264abb93eaeff43ad7/pygame-2.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c", size = 13951197 },
+ { url = "https://files.pythonhosted.org/packages/a6/06/29e939b34d3f1354738c7d201c51c250ad7abefefaf6f8332d962ff67c4b/pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e", size = 10249309 },
+ { url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084 },
+]
+
+[[package]]
+name = "pytest"
+version = "8.3.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
+]
+
+[[package]]
+name = "pytest-trio"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "outcome" },
+ { name = "pytest" },
+ { name = "trio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/13/08/056279526554c6c6e6ad6d4a479a338d14dc785ac30be8bdc6ca0153c1be/pytest-trio-0.8.0.tar.gz", hash = "sha256:8363db6336a79e6c53375a2123a41ddbeccc4aa93f93788651641789a56fb52e", size = 46525 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/22/71953f47e0da5852c899f58cd7a31e6100f37c632b7b9ee52d067613a844/pytest_trio-0.8.0-py3-none-any.whl", hash = "sha256:e6a7e7351ae3e8ec3f4564d30ee77d1ec66e1df611226e5618dbb32f9545c841", size = 27221 },
+]
+
+[[package]]
+name = "ruff"
+version = "0.9.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 },
+ { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 },
+ { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 },
+ { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 },
+ { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 },
+ { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 },
+ { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 },
+ { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 },
+ { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 },
+ { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 },
+ { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 },
+ { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 },
+ { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 },
+ { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 },
+ { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 },
+ { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 },
+ { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
+]
+
+[[package]]
+name = "sortedcontainers"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
+]
+
+[[package]]
+name = "trio"
+version = "0.28.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "outcome" },
+ { name = "sniffio" },
+ { name = "sortedcontainers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/73/57efab729506a8d4b89814f1e356ec8f3369de0ed4fd7e7616974d09646d/trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05", size = 580318 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/04/9954a59e1fb6732f5436225c9af963811d7b24ea62a8bf96991f2cb8c26e/trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94", size = 486317 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
+]
+
+[[package]]
+name = "uv"
+version = "0.5.21"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/41/c1/d8da0122d14a48a7895241b1c15b027d7df6f56350cae614561c0567ecb2/uv-0.5.21.tar.gz", hash = "sha256:eb33043b42111ae3fef76906422b5c4247188e1ae1233da63be82cc64bb527d0", size = 2631880 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/c7/c7a787cc2c526442b2999cbebe526e24517b8812f3d545e90811e38c213a/uv-0.5.21-py3-none-linux_armv6l.whl", hash = "sha256:8ea7309dc1891e88276e207aa389cc4524ec7a7038a75bfd7c5a09ed3701316f", size = 15181071 },
+ { url = "https://files.pythonhosted.org/packages/5d/61/5a6796f31830898d0aa01e018d49bbbf39d61f2c19350663be16b6cfd1d9/uv-0.5.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef4e579390a022efcbfe8720f51ad46fdff54caf982782967d5689841485ddd8", size = 15305687 },
+ { url = "https://files.pythonhosted.org/packages/65/37/a5a2e0d0776063e2fe1f6dfac21dd5e707d2df9c167572c416970dd3af34/uv-0.5.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:73c9d1bdbff989114c5c37649235c569f89b65bd2e57b75d8fdb73946ade7cbd", size = 14214520 },
+ { url = "https://files.pythonhosted.org/packages/15/ce/a844df3ea81c9370feed1ab0fd474776709a60f07b897c41fcdf0f260c0f/uv-0.5.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:6e97c68306c0583af1b14b5b801c3e18ab7bc349a4c9cdd8ab5f8f46348539c5", size = 14667101 },
+ { url = "https://files.pythonhosted.org/packages/88/53/d4a0cefd1927f6047500c95967d69d045b11839c9f48e2a448372498186f/uv-0.5.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ecdf58adf9376f2b4f63e6538e38be0e77fcd3d5b07b3ee56a3c7cd1d9ca526", size = 14952637 },
+ { url = "https://files.pythonhosted.org/packages/4d/0a/a68d9142e429b4a28cebcae21c6dba262f7905772d950d076e0b161f4e0c/uv-0.5.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dafa7b5bb3ae8949ba100645b7a8d804f683547586024f73ad1b2d97a1aa9976", size = 15665199 },
+ { url = "https://files.pythonhosted.org/packages/18/9a/062eb481fe3661ee663751f0af9a6490014357592c9aea65d0261d385a40/uv-0.5.21-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:609299c04c00ece874b30abee9cb83753224a03e8d9191327397f33a92674a53", size = 16571172 },
+ { url = "https://files.pythonhosted.org/packages/94/f0/8e36e40acb289a39ed00a49122f6c3ad36993ff11d8197885877ace30b73/uv-0.5.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10232d5f24a1831f7ab3967f0b56f78681c520ff3391dcf5096eface94619e8e", size = 16292510 },
+ { url = "https://files.pythonhosted.org/packages/91/40/3b48d57626dcb306c9e5736d4148fb6eaf931d94dbeb810ad32e48b58ac8/uv-0.5.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f17d35ab4a099657ad55d3cfeaf91a35b929ae2cd2b22163710cdfec45ea3941", size = 20623325 },
+ { url = "https://files.pythonhosted.org/packages/5c/6f/86ee925f5e20df3aa366538a56e0d1bd5dfa9ef9d9bea57709480d47d72c/uv-0.5.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a1582f4964b1249b0e82ad0e60519a73392e099541a6db587e7333139255d50", size = 15952215 },
+ { url = "https://files.pythonhosted.org/packages/62/f9/094ceaf8f0380b5381918aeb65907ff1fd06150b51f3baafa879ed9fdf4a/uv-0.5.21-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:afd98237d97b92935c8d5a9bf28218b5ecb497af9a99ad0a740d0b71b51f864a", size = 14914771 },
+ { url = "https://files.pythonhosted.org/packages/0c/10/a5f73f433f29922b304eb95e7d6f18632734f92753c73017a8b05ce41795/uv-0.5.21-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:b317bfb7ba61e0396be5776f723e03e818a6393322f62828b67c16b565e1c0ec", size = 14904317 },
+ { url = "https://files.pythonhosted.org/packages/76/4e/b9be4fcc45a026f1e1a2975719ee5f0444dafda1b606c0871d0c24651115/uv-0.5.21-py3-none-musllinux_1_1_i686.whl", hash = "sha256:168fca3bad68f75518a168feeebfd2c0b104e9abc06a33caa710d0b2753db3aa", size = 15315311 },
+ { url = "https://files.pythonhosted.org/packages/a5/2d/74df7f292a7c15269bacd451a492520e26c4ef99b19c01fe96913506dea5/uv-0.5.21-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f5ba5076b6b69161d318f5ddeff6dd935ab29a157ff10dd8756ed6dcb5d0a497", size = 16042115 },
+ { url = "https://files.pythonhosted.org/packages/4b/69/03731b38d23e7bed653f186be2ff2dfcdcef29a611f4937ff4bacff205fe/uv-0.5.21-py3-none-win32.whl", hash = "sha256:34944204a39b840fa0efb2ba27f4decce50115460c6b8e4e6ade6aae6246d0cf", size = 15262952 },
+ { url = "https://files.pythonhosted.org/packages/c2/8d/f6508e3c3fbc76b945365062ffff9fa6e60ad6516b26dae23a1c761d65c0/uv-0.5.21-py3-none-win_amd64.whl", hash = "sha256:36f21534a9e00a85cc532ef9575d3785a4e434a25daa93e51ebc08b54ade4991", size = 16625459 },
+]
From e783b16390d10b41ab65766aa98e1911913395ea Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 18 Jan 2025 17:59:06 -0600
Subject: [PATCH 44/67] Fix activation command
---
ci.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ci.sh b/ci.sh
index 6fb887f..f470dd3 100755
--- a/ci.sh
+++ b/ci.sh
@@ -30,7 +30,7 @@ UV_VENV_OUTPUT="$(uv venv --seed --allow-existing 2>&1)"
echo "$UV_VENV_OUTPUT"
# Extract the activation command from the output
-activation_command=$(echo "$UV_VENV_OUTPUT" | grep -oP '(?<=Activate with: ).*')
+activation_command=$(echo "$UV_VENV_OUTPUT" | grep 'Activate with:' | sed 's/.*Activate with: //')
# Check if the activation command was found
if [ -n "$activation_command" ]; then
From 441a53350d947d7883f139d4ffc784ed6518da01 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 18 Jan 2025 18:02:09 -0600
Subject: [PATCH 45/67] Try to fix activation script again
---
ci.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ci.sh b/ci.sh
index f470dd3..7dd39ba 100755
--- a/ci.sh
+++ b/ci.sh
@@ -30,7 +30,7 @@ UV_VENV_OUTPUT="$(uv venv --seed --allow-existing 2>&1)"
echo "$UV_VENV_OUTPUT"
# Extract the activation command from the output
-activation_command=$(echo "$UV_VENV_OUTPUT" | grep 'Activate with:' | sed 's/.*Activate with: //')
+activation_command=$(echo "$UV_VENV_OUTPUT" | grep 'Activate with:' | sed 's/.*Activate with: //; s/^/"/; s/$/"/')
# Check if the activation command was found
if [ -n "$activation_command" ]; then
From 881f454bf644055815fb457517cebdf5b9be1856 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 18 Jan 2025 18:11:20 -0600
Subject: [PATCH 46/67] Run different virtual environment activate depending on
platform
---
ci.sh | 37 +++++++++++++++++++++++--------------
1 file changed, 23 insertions(+), 14 deletions(-)
diff --git a/ci.sh b/ci.sh
index 7dd39ba..af2d284 100755
--- a/ci.sh
+++ b/ci.sh
@@ -26,21 +26,30 @@ python -m pip install uv==$UV_VERSION
python -m uv --version
UV_VENV_SEED="pip"
-UV_VENV_OUTPUT="$(uv venv --seed --allow-existing 2>&1)"
-echo "$UV_VENV_OUTPUT"
-
-# Extract the activation command from the output
-activation_command=$(echo "$UV_VENV_OUTPUT" | grep 'Activate with:' | sed 's/.*Activate with: //; s/^/"/; s/$/"/')
-
-# Check if the activation command was found
-if [ -n "$activation_command" ]; then
- # Execute the activation command
- echo "Activating virtual environment..."
- eval "$activation_command"
-else
- echo "::error:: Activation command not found in uv venv output."
+python -m uv venv --seed --allow-existing
+
+# Determine the platform and activate the virtual environment accordingly
+case "$OSTYPE" in
+ linux-gnu*|linux-musl*)
+ # Linux
+ echo "Activating virtual environment on Linux..."
+ source .venv/bin/activate
+ ;;
+ darwin*)
+ # macOS
+ echo "Activating virtual environment on macOS..."
+ source .venv/bin/activate
+ ;;
+ cygwin*|msys*)
+ # Windows
+ echo "Activating virtual environment on Windows..."
+ .venv\Scripts\activate
+ ;;
+ *)
+ echo "::error:: Unknown OS. Please activate the virtual environment manually."
exit 1
-fi
+ ;;
+esac
python -m pip install uv==$UV_VERSION
# python -m uv build
From c4d9d794c3af51449175e6e1dc74e4da179360e5 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 18 Jan 2025 18:20:57 -0600
Subject: [PATCH 47/67] Try to fix windows activate
---
ci.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ci.sh b/ci.sh
index af2d284..e6cfdfe 100755
--- a/ci.sh
+++ b/ci.sh
@@ -40,10 +40,10 @@ case "$OSTYPE" in
echo "Activating virtual environment on macOS..."
source .venv/bin/activate
;;
- cygwin*|msys*)
+ cygwin*|msys*|win32)
# Windows
echo "Activating virtual environment on Windows..."
- .venv\Scripts\activate
+ source .venv/Scripts/activate
;;
*)
echo "::error:: Unknown OS. Please activate the virtual environment manually."
From 851bba748daa83547c7f3f348bdc729251f7b127 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 18 Jan 2025 18:23:30 -0600
Subject: [PATCH 48/67] Remove unused case
---
ci.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ci.sh b/ci.sh
index e6cfdfe..f31e38a 100755
--- a/ci.sh
+++ b/ci.sh
@@ -40,7 +40,7 @@ case "$OSTYPE" in
echo "Activating virtual environment on macOS..."
source .venv/bin/activate
;;
- cygwin*|msys*|win32)
+ cygwin*|msys*)
# Windows
echo "Activating virtual environment on Windows..."
source .venv/Scripts/activate
From 48d3793357b237e0733ebbe26080a3876ad84adb Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 18 Jan 2025 18:26:28 -0600
Subject: [PATCH 49/67] Cleanup
---
ci.sh | 23 ++++-------------------
1 file changed, 4 insertions(+), 19 deletions(-)
diff --git a/ci.sh b/ci.sh
index f31e38a..e7d4c92 100755
--- a/ci.sh
+++ b/ci.sh
@@ -30,31 +30,20 @@ python -m uv venv --seed --allow-existing
# Determine the platform and activate the virtual environment accordingly
case "$OSTYPE" in
- linux-gnu*|linux-musl*)
- # Linux
- echo "Activating virtual environment on Linux..."
- source .venv/bin/activate
- ;;
- darwin*)
- # macOS
- echo "Activating virtual environment on macOS..."
+ linux-gnu*|linux-musl*|darwin*)
source .venv/bin/activate
;;
cygwin*|msys*)
- # Windows
- echo "Activating virtual environment on Windows..."
source .venv/Scripts/activate
;;
*)
- echo "::error:: Unknown OS. Please activate the virtual environment manually."
+ echo "::error:: Unknown OS. Please add an activation method for '$OSTYPE'."
exit 1
;;
esac
-python -m pip install uv==$UV_VERSION
-# python -m uv build
-# wheel_package=$(ls dist/*.whl)
-# python -m uv pip install "$PROJECT @ $wheel_package" -c test-requirements.txt
+# Install uv in virtual environment
+python -m pip install uv==$UV_VERSION
if [ "$CHECK_FORMATTING" = "1" ]; then
python -m uv sync --extra tests --extra tools
@@ -65,7 +54,6 @@ else
# expands to 0 != 1 if NO_TEST_REQUIREMENTS is not set, if set the `-0` has no effect
# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02
if [ "${NO_TEST_REQUIREMENTS-0}" == 1 ]; then
- # python -m uv pip install pytest coverage -c test-requirements.txt
python -m uv sync --extra tests
flags=""
#"--skip-optional-imports"
@@ -90,9 +78,6 @@ else
# get mypy tests a nice cache
MYPYPATH=".." mypy --config-file= --cache-dir=./.mypy_cache -c "import $PROJECT" >/dev/null 2>/dev/null || true
- # support subprocess spawning with coverage.py
- # echo "import coverage; coverage.process_startup()" | tee -a "$INSTALLDIR/../sitecustomize.py"
-
echo "::endgroup::"
echo "::group:: Run Tests"
if coverage run --rcfile=../pyproject.toml -m pytest -ra --junitxml=../test-results.xml ../tests --verbose --durations=10 $flags; then
From 1555747966239e4adf37bf29d24ec836fb9ce188 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Thu, 13 Feb 2025 10:46:12 -0600
Subject: [PATCH 50/67] Update depencencies & minor CI changes
---
.github/workflows/autodeps.yml | 2 +-
.pre-commit-config.yaml | 12 +-
ci.sh | 8 +-
uv.lock | 368 +++++++++++++++++----------------
4 files changed, 199 insertions(+), 191 deletions(-)
diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml
index a41f4ff..1862fb9 100644
--- a/.github/workflows/autodeps.yml
+++ b/.github/workflows/autodeps.yml
@@ -40,7 +40,7 @@ jobs:
# apply newer versions' formatting
- name: Pre-commit updates
- run: uv run pre-commit run -a
+ run: uv run pre-commit run -a || true
- name: Commit changes and create automerge PR
env:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e689c73..3f9da0e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,33 +16,35 @@ repos:
- id: check-merge-conflict
- id: mixed-line-ending
- id: check-case-conflict
+ - id: check-added-large-files
- id: sort-simple-yaml
files: .pre-commit-config.yaml
- repo: https://github.com/psf/black-pre-commit-mirror
- rev: 24.10.0
+ rev: 25.1.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.9.2
+ rev: v0.9.6
hooks:
- id: ruff
types: [file]
types_or: [python, pyi, toml]
+ args: ["--show-fixes"]
- repo: https://github.com/CoolCat467/badgie
rev: v0.9.6
hooks:
- id: badgie
- repo: https://github.com/codespell-project/codespell
- rev: v2.3.0
+ rev: v2.4.1
hooks:
- id: codespell
additional_dependencies:
- tomli
- repo: https://github.com/crate-ci/typos
- rev: dictgen-v0.3.1
+ rev: typos-dict-v0.12.5
hooks:
- id: typos
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v1.2.1
+ rev: v1.3.1
hooks:
- id: zizmor
diff --git a/ci.sh b/ci.sh
index e7d4c92..02ea341 100755
--- a/ci.sh
+++ b/ci.sh
@@ -28,7 +28,7 @@ python -m uv --version
UV_VENV_SEED="pip"
python -m uv venv --seed --allow-existing
-# Determine the platform and activate the virtual environment accordingly
+# Determine platform and activate virtual environment accordingly
case "$OSTYPE" in
linux-gnu*|linux-musl*|darwin*)
source .venv/bin/activate
@@ -46,7 +46,7 @@ esac
python -m pip install uv==$UV_VERSION
if [ "$CHECK_FORMATTING" = "1" ]; then
- python -m uv sync --extra tests --extra tools
+ python -m uv sync --locked --extra tests --extra tools
echo "::endgroup::"
source check.sh
else
@@ -54,11 +54,11 @@ else
# expands to 0 != 1 if NO_TEST_REQUIREMENTS is not set, if set the `-0` has no effect
# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02
if [ "${NO_TEST_REQUIREMENTS-0}" == 1 ]; then
- python -m uv sync --extra tests
+ python -m uv sync --locked --extra tests
flags=""
#"--skip-optional-imports"
else
- python -m uv sync --extra tests --extra tools
+ python -m uv sync --locked --extra tests --extra tools
flags=""
fi
diff --git a/uv.lock b/uv.lock
index a9fa758..c3cebe9 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,11 +3,11 @@ requires-python = ">=3.10"
[[package]]
name = "attrs"
-version = "24.3.0"
+version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 }
+sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 },
+ { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 },
]
[[package]]
@@ -60,7 +60,7 @@ requires-dist = [
[[package]]
name = "black"
-version = "24.10.0"
+version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -71,25 +71,25 @@ dependencies = [
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813 }
+sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a3/f3/465c0eb5cddf7dbbfe1fecd9b875d1dcf51b88923cd2c1d7e9ab95c6336b/black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", size = 1623211 },
- { url = "https://files.pythonhosted.org/packages/df/57/b6d2da7d200773fdfcc224ffb87052cf283cec4d7102fab450b4a05996d8/black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", size = 1457139 },
- { url = "https://files.pythonhosted.org/packages/6e/c5/9023b7673904a5188f9be81f5e129fff69f51f5515655fbd1d5a4e80a47b/black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", size = 1753774 },
- { url = "https://files.pythonhosted.org/packages/e1/32/df7f18bd0e724e0d9748829765455d6643ec847b3f87e77456fc99d0edab/black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e", size = 1414209 },
- { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468 },
- { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270 },
- { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061 },
- { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293 },
- { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256 },
- { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534 },
- { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892 },
- { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796 },
- { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986 },
- { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085 },
- { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928 },
- { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875 },
- { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898 },
+ { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 },
+ { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 },
+ { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 },
+ { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 },
+ { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 },
+ { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 },
+ { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 },
+ { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 },
+ { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 },
+ { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 },
+ { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 },
+ { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 },
+ { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
+ { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
+ { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
+ { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
+ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
]
[[package]]
@@ -163,11 +163,11 @@ wheels = [
[[package]]
name = "codespell"
-version = "2.3.0"
+version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a0/a9/98353dfc7afcdf18cffd2dd3e959a25eaaf2728cf450caa59af89648a8e4/codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f", size = 329791 }
+sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0e/20/b6019add11e84f821184234cea0ad91442373489ef7ccfa3d73a71b908fa/codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1", size = 329167 },
+ { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501 },
]
[[package]]
@@ -181,61 +181,62 @@ wheels = [
[[package]]
name = "coverage"
-version = "7.6.10"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c5/12/2a2a923edf4ddabdffed7ad6da50d96a5c126dae7b80a33df7310e329a1e/coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", size = 207982 },
- { url = "https://files.pythonhosted.org/packages/ca/49/6985dbca9c7be3f3cb62a2e6e492a0c88b65bf40579e16c71ae9c33c6b23/coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", size = 208414 },
- { url = "https://files.pythonhosted.org/packages/35/93/287e8f1d1ed2646f4e0b2605d14616c9a8a2697d0d1b453815eb5c6cebdb/coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", size = 236860 },
- { url = "https://files.pythonhosted.org/packages/de/e1/cfdb5627a03567a10031acc629b75d45a4ca1616e54f7133ca1fa366050a/coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", size = 234758 },
- { url = "https://files.pythonhosted.org/packages/6d/85/fc0de2bcda3f97c2ee9fe8568f7d48f7279e91068958e5b2cc19e0e5f600/coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", size = 235920 },
- { url = "https://files.pythonhosted.org/packages/79/73/ef4ea0105531506a6f4cf4ba571a214b14a884630b567ed65b3d9c1975e1/coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", size = 234986 },
- { url = "https://files.pythonhosted.org/packages/c6/4d/75afcfe4432e2ad0405c6f27adeb109ff8976c5e636af8604f94f29fa3fc/coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", size = 233446 },
- { url = "https://files.pythonhosted.org/packages/86/5b/efee56a89c16171288cafff022e8af44f8f94075c2d8da563c3935212871/coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", size = 234566 },
- { url = "https://files.pythonhosted.org/packages/f2/db/67770cceb4a64d3198bf2aa49946f411b85ec6b0a9b489e61c8467a4253b/coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", size = 210675 },
- { url = "https://files.pythonhosted.org/packages/8d/27/e8bfc43f5345ec2c27bc8a1fa77cdc5ce9dcf954445e11f14bb70b889d14/coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", size = 211518 },
- { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 },
- { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 },
- { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 },
- { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 },
- { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 },
- { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 },
- { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 },
- { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 },
- { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 },
- { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 },
- { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 },
- { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 },
- { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 },
- { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 },
- { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 },
- { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 },
- { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 },
- { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 },
- { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 },
- { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 },
- { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 },
- { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 },
- { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 },
- { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 },
- { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 },
- { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 },
- { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 },
- { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 },
- { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 },
- { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 },
- { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 },
- { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 },
- { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 },
- { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 },
- { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 },
- { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 },
- { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 },
- { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 },
- { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 },
- { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 },
- { url = "https://files.pythonhosted.org/packages/a1/70/de81bfec9ed38a64fc44a77c7665e20ca507fc3265597c28b0d989e4082e/coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f", size = 200223 },
+version = "7.6.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/67/81dc41ec8f548c365d04a29f1afd492d3176b372c33e47fa2a45a01dc13a/coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8", size = 208345 },
+ { url = "https://files.pythonhosted.org/packages/33/43/17f71676016c8829bde69e24c852fef6bd9ed39f774a245d9ec98f689fa0/coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879", size = 208775 },
+ { url = "https://files.pythonhosted.org/packages/86/25/c6ff0775f8960e8c0840845b723eed978d22a3cd9babd2b996e4a7c502c6/coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe", size = 237925 },
+ { url = "https://files.pythonhosted.org/packages/b0/3d/5f5bd37046243cb9d15fff2c69e498c2f4fe4f9b42a96018d4579ed3506f/coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674", size = 235835 },
+ { url = "https://files.pythonhosted.org/packages/b5/f1/9e6b75531fe33490b910d251b0bf709142e73a40e4e38a3899e6986fe088/coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb", size = 236966 },
+ { url = "https://files.pythonhosted.org/packages/4f/bc/aef5a98f9133851bd1aacf130e754063719345d2fb776a117d5a8d516971/coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c", size = 236080 },
+ { url = "https://files.pythonhosted.org/packages/eb/d0/56b4ab77f9b12aea4d4c11dc11cdcaa7c29130b837eb610639cf3400c9c3/coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c", size = 234393 },
+ { url = "https://files.pythonhosted.org/packages/0d/77/28ef95c5d23fe3dd191a0b7d89c82fea2c2d904aef9315daf7c890e96557/coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e", size = 235536 },
+ { url = "https://files.pythonhosted.org/packages/29/62/18791d3632ee3ff3f95bc8599115707d05229c72db9539f208bb878a3d88/coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425", size = 211063 },
+ { url = "https://files.pythonhosted.org/packages/fc/57/b3878006cedfd573c963e5c751b8587154eb10a61cc0f47a84f85c88a355/coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa", size = 211955 },
+ { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 },
+ { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 },
+ { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 },
+ { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 },
+ { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 },
+ { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 },
+ { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 },
+ { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 },
+ { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 },
+ { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 },
+ { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 },
+ { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 },
+ { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 },
+ { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 },
+ { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 },
+ { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 },
+ { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 },
+ { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 },
+ { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 },
+ { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 },
+ { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 },
+ { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 },
+ { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 },
+ { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 },
+ { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 },
+ { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 },
+ { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 },
+ { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 },
+ { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 },
+ { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 },
+ { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 },
+ { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 },
+ { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 },
+ { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 },
+ { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 },
+ { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 },
+ { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 },
+ { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 },
+ { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 },
+ { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 },
+ { url = "https://files.pythonhosted.org/packages/7a/7f/05818c62c7afe75df11e0233bd670948d68b36cdbf2a339a095bc02624a8/coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf", size = 200558 },
+ { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 },
]
[package.optional-dependencies]
@@ -245,39 +246,43 @@ toml = [
[[package]]
name = "cryptography"
-version = "44.0.0"
+version = "44.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 },
- { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 },
- { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 },
- { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 },
- { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 },
- { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 },
- { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 },
- { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 },
- { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 },
- { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 },
- { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 },
- { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 },
- { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 },
- { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 },
- { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 },
- { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 },
- { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 },
- { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 },
- { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 },
- { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 },
- { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857 },
- { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615 },
- { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622 },
- { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546 },
- { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937 },
- { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 },
+sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022 },
+ { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 },
+ { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 },
+ { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 },
+ { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 },
+ { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 },
+ { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 },
+ { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 },
+ { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 },
+ { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 },
+ { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882 },
+ { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989 },
+ { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714 },
+ { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 },
+ { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 },
+ { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 },
+ { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 },
+ { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 },
+ { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 },
+ { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 },
+ { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 },
+ { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 },
+ { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371 },
+ { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303 },
+ { url = "https://files.pythonhosted.org/packages/15/06/507bfb5c7e048114a0185dd65f7814677a2ba285d15705c3d69e660c21d7/cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183", size = 3380782 },
+ { url = "https://files.pythonhosted.org/packages/e0/f1/7fb4982d59aa86e1a116c812b545e7fc045352be07738ae3fb278835a9a4/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", size = 3888155 },
+ { url = "https://files.pythonhosted.org/packages/60/7b/cbc203838d3092203493d18b923fbbb1de64e0530b332a713ba376905b0b/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", size = 4106417 },
+ { url = "https://files.pythonhosted.org/packages/12/c7/2fe59fb085ab418acc82e91e040a6acaa7b1696fcc1c1055317537fbf0d3/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", size = 3887540 },
+ { url = "https://files.pythonhosted.org/packages/48/89/09fc7b115f60f5bd970b80e32244f8e9aeeb9244bf870b63420cec3b5cd5/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", size = 4106040 },
+ { url = "https://files.pythonhosted.org/packages/2e/38/3fd83c4690dc7d753a442a284b3826ea5e5c380a411443c66421cd823898/cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", size = 3134657 },
]
[[package]]
@@ -309,7 +314,7 @@ wheels = [
[[package]]
name = "libcomponent"
-version = "0.0.1"
+version = "0.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
@@ -317,47 +322,47 @@ dependencies = [
{ name = "trio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/40/30e51ad19bc3420a61e60c300c2bc4a9f685ba587bdcf8e852dd5a3a9a40/libcomponent-0.0.1.tar.gz", hash = "sha256:fdebdc9b4857707511c3b2bf0a434af30910b19fa0ec1ddb3e78457d074e40f1", size = 74693 }
+sdist = { url = "https://files.pythonhosted.org/packages/2f/e9/7a72fba139f5d40686c2ca35bb9ce7417b0fa48d14206e5df9997636e5a9/libcomponent-0.0.2.tar.gz", hash = "sha256:cd337db0b435d69e723c8475be0df84f5fbed73756f08090e237fb51a7f197a3", size = 75171 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b1/2f/404652eae9da1f1216c66e57d016debaec8ec927fcd2a2c3c600629ceb25/libcomponent-0.0.1-py3-none-any.whl", hash = "sha256:be0fe9fffcebe4602656d55fb1c1d8afa230552d84cf1d69300b23d44e19369a", size = 57644 },
+ { url = "https://files.pythonhosted.org/packages/81/ff/1626669a3ece34c6966750ef5623165947eb3c94f35598a309e9bac870f7/libcomponent-0.0.2-py3-none-any.whl", hash = "sha256:1bfd730d2044349005dc796c624b239a9799fcf01272a8f6a0c43372e2751047", size = 57907 },
]
[[package]]
name = "mypy"
-version = "1.14.1"
+version = "1.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002 },
- { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400 },
- { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172 },
- { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732 },
- { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197 },
- { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836 },
- { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 },
- { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 },
- { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 },
- { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 },
- { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 },
- { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 },
- { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 },
- { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 },
- { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 },
- { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 },
- { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 },
- { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 },
- { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 },
- { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 },
- { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 },
- { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 },
- { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 },
- { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 },
- { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 },
+sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 },
+ { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 },
+ { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 },
+ { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 },
+ { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 },
+ { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 },
+ { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 },
+ { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 },
+ { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 },
+ { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 },
+ { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 },
+ { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 },
+ { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 },
+ { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 },
+ { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 },
+ { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 },
+ { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 },
+ { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 },
+ { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
+ { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
+ { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
+ { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
+ { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
+ { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
+ { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
]
[[package]]
@@ -630,27 +635,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.9.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 },
- { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 },
- { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 },
- { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 },
- { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 },
- { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 },
- { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 },
- { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 },
- { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 },
- { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 },
- { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 },
- { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 },
- { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 },
- { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 },
- { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 },
- { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 },
- { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 },
+version = "0.9.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 },
+ { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 },
+ { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 },
+ { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 },
+ { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 },
+ { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 },
+ { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 },
+ { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 },
+ { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 },
+ { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 },
+ { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 },
+ { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 },
+ { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 },
+ { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 },
+ { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 },
+ { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 },
+ { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 },
]
[[package]]
@@ -739,24 +744,25 @@ wheels = [
[[package]]
name = "uv"
-version = "0.5.21"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/41/c1/d8da0122d14a48a7895241b1c15b027d7df6f56350cae614561c0567ecb2/uv-0.5.21.tar.gz", hash = "sha256:eb33043b42111ae3fef76906422b5c4247188e1ae1233da63be82cc64bb527d0", size = 2631880 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c5/c7/c7a787cc2c526442b2999cbebe526e24517b8812f3d545e90811e38c213a/uv-0.5.21-py3-none-linux_armv6l.whl", hash = "sha256:8ea7309dc1891e88276e207aa389cc4524ec7a7038a75bfd7c5a09ed3701316f", size = 15181071 },
- { url = "https://files.pythonhosted.org/packages/5d/61/5a6796f31830898d0aa01e018d49bbbf39d61f2c19350663be16b6cfd1d9/uv-0.5.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef4e579390a022efcbfe8720f51ad46fdff54caf982782967d5689841485ddd8", size = 15305687 },
- { url = "https://files.pythonhosted.org/packages/65/37/a5a2e0d0776063e2fe1f6dfac21dd5e707d2df9c167572c416970dd3af34/uv-0.5.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:73c9d1bdbff989114c5c37649235c569f89b65bd2e57b75d8fdb73946ade7cbd", size = 14214520 },
- { url = "https://files.pythonhosted.org/packages/15/ce/a844df3ea81c9370feed1ab0fd474776709a60f07b897c41fcdf0f260c0f/uv-0.5.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:6e97c68306c0583af1b14b5b801c3e18ab7bc349a4c9cdd8ab5f8f46348539c5", size = 14667101 },
- { url = "https://files.pythonhosted.org/packages/88/53/d4a0cefd1927f6047500c95967d69d045b11839c9f48e2a448372498186f/uv-0.5.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ecdf58adf9376f2b4f63e6538e38be0e77fcd3d5b07b3ee56a3c7cd1d9ca526", size = 14952637 },
- { url = "https://files.pythonhosted.org/packages/4d/0a/a68d9142e429b4a28cebcae21c6dba262f7905772d950d076e0b161f4e0c/uv-0.5.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dafa7b5bb3ae8949ba100645b7a8d804f683547586024f73ad1b2d97a1aa9976", size = 15665199 },
- { url = "https://files.pythonhosted.org/packages/18/9a/062eb481fe3661ee663751f0af9a6490014357592c9aea65d0261d385a40/uv-0.5.21-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:609299c04c00ece874b30abee9cb83753224a03e8d9191327397f33a92674a53", size = 16571172 },
- { url = "https://files.pythonhosted.org/packages/94/f0/8e36e40acb289a39ed00a49122f6c3ad36993ff11d8197885877ace30b73/uv-0.5.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10232d5f24a1831f7ab3967f0b56f78681c520ff3391dcf5096eface94619e8e", size = 16292510 },
- { url = "https://files.pythonhosted.org/packages/91/40/3b48d57626dcb306c9e5736d4148fb6eaf931d94dbeb810ad32e48b58ac8/uv-0.5.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f17d35ab4a099657ad55d3cfeaf91a35b929ae2cd2b22163710cdfec45ea3941", size = 20623325 },
- { url = "https://files.pythonhosted.org/packages/5c/6f/86ee925f5e20df3aa366538a56e0d1bd5dfa9ef9d9bea57709480d47d72c/uv-0.5.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a1582f4964b1249b0e82ad0e60519a73392e099541a6db587e7333139255d50", size = 15952215 },
- { url = "https://files.pythonhosted.org/packages/62/f9/094ceaf8f0380b5381918aeb65907ff1fd06150b51f3baafa879ed9fdf4a/uv-0.5.21-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:afd98237d97b92935c8d5a9bf28218b5ecb497af9a99ad0a740d0b71b51f864a", size = 14914771 },
- { url = "https://files.pythonhosted.org/packages/0c/10/a5f73f433f29922b304eb95e7d6f18632734f92753c73017a8b05ce41795/uv-0.5.21-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:b317bfb7ba61e0396be5776f723e03e818a6393322f62828b67c16b565e1c0ec", size = 14904317 },
- { url = "https://files.pythonhosted.org/packages/76/4e/b9be4fcc45a026f1e1a2975719ee5f0444dafda1b606c0871d0c24651115/uv-0.5.21-py3-none-musllinux_1_1_i686.whl", hash = "sha256:168fca3bad68f75518a168feeebfd2c0b104e9abc06a33caa710d0b2753db3aa", size = 15315311 },
- { url = "https://files.pythonhosted.org/packages/a5/2d/74df7f292a7c15269bacd451a492520e26c4ef99b19c01fe96913506dea5/uv-0.5.21-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f5ba5076b6b69161d318f5ddeff6dd935ab29a157ff10dd8756ed6dcb5d0a497", size = 16042115 },
- { url = "https://files.pythonhosted.org/packages/4b/69/03731b38d23e7bed653f186be2ff2dfcdcef29a611f4937ff4bacff205fe/uv-0.5.21-py3-none-win32.whl", hash = "sha256:34944204a39b840fa0efb2ba27f4decce50115460c6b8e4e6ade6aae6246d0cf", size = 15262952 },
- { url = "https://files.pythonhosted.org/packages/c2/8d/f6508e3c3fbc76b945365062ffff9fa6e60ad6516b26dae23a1c761d65c0/uv-0.5.21-py3-none-win_amd64.whl", hash = "sha256:36f21534a9e00a85cc532ef9575d3785a4e434a25daa93e51ebc08b54ade4991", size = 16625459 },
+version = "0.5.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/67/04/63265828848c2ca60e322408ed529587f670ee97c5607114df08c389398a/uv-0.5.31.tar.gz", hash = "sha256:59c4c6e3704208a8dd5e8d51b79ec995db18a64bd3ff88fd239ca433fbaf1694", size = 2875508 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/40/80/58942f09e0a38fdef37d54d553175e3c195da32547711c78dcc70876f2ce/uv-0.5.31-py3-none-linux_armv6l.whl", hash = "sha256:ba5707a6e363284ba1acd29ae9e70e2377ed31e272b953069798c444bae847ef", size = 15475386 },
+ { url = "https://files.pythonhosted.org/packages/a2/ed/1605df7bd74eac86975a48e16a76ae04feedc9d27dc841e8d4f3c00a790f/uv-0.5.31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3169a373d0d41571a7b9d4a442f875f6e26250693ced7779f62461f52ba1da64", size = 15608043 },
+ { url = "https://files.pythonhosted.org/packages/1f/5a/1eb42f481a9f9010c8c194d70ab375a6eda96d67ca1fd011bf869d4016c8/uv-0.5.31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:335c16f91b46b4f4a3b31c18cf112a0643d59d4c1708a177103621da0addbaef", size = 14523527 },
+ { url = "https://files.pythonhosted.org/packages/9a/05/9817ea1f0d8e7134ed60abeefa7bdc602f4a4a6e2ccdf2760b54fb3dcef3/uv-0.5.31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:cedceefebf2123b514464671d0544a8db126071c2d56dbc10d408b8222939e6a", size = 14940294 },
+ { url = "https://files.pythonhosted.org/packages/e4/2d/aee8e68026057c6db71424e3a312d739af8838ae35321bfa1f5900e93d1c/uv-0.5.31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7233182a2b8226011562341f05aaee19925b48730fccdb2e7ee20e31a84f12db", size = 15211133 },
+ { url = "https://files.pythonhosted.org/packages/58/cf/16c3b71c903e7d8c3aeb0b85efbf2efb4694ffeab72165d7d9166bf2d497/uv-0.5.31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ce4dc079fd5ddf1946e6085b6ece126ce7c4be23ba27e4010aa68fdec004191", size = 15943734 },
+ { url = "https://files.pythonhosted.org/packages/7c/28/8421b94710581c81a9240df95f04b87cfffd5da229eb178733acb6d1a6de/uv-0.5.31-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:007576e1b62268d4a21d4a375d43ff5ae3698313a11f7702c8e7cb5bd29d7f1b", size = 16890117 },
+ { url = "https://files.pythonhosted.org/packages/be/9c/a3d4318aebbc68158dc069d3f8de423d56ec3a38017401e92e9e37fe5afc/uv-0.5.31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51d8287cdb760ea8c44b374cb96a59fae2292f1b3e18e228f7ed817d2bd96243", size = 16623168 },
+ { url = "https://files.pythonhosted.org/packages/dd/b1/32a5e1239eca3915bec3825dab8c635f80c64b09ae46cf03d1bef7641892/uv-0.5.31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27ce8f3eecd281a6ec255644a328b60eb10044e506a46be931db7bbfe8db89ab", size = 20939390 },
+ { url = "https://files.pythonhosted.org/packages/ce/2e/0c3ac2f5be92492cbe735de7f66a83b2d3e22bd59554deaa0106562cba45/uv-0.5.31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d07e9db12a55005a28bb49ecfa444a0221702158fc021f79e26d8e174f1ebdf9", size = 16293460 },
+ { url = "https://files.pythonhosted.org/packages/cc/de/59e6665d9f1d4fc93c0b3383eaf31dbf7088cf8fce5c239b5eb8f0bf911b/uv-0.5.31-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8acf6bcb0c0c27e1a157926f35dc70b1c7620c1a2e1124ffacdbf21c78265761", size = 15234496 },
+ { url = "https://files.pythonhosted.org/packages/32/14/e69d04bc77f73a34d2d850d60cf21ded8cf0f3481302ea31533ad5a64733/uv-0.5.31-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:a8f27ea8441ce9de43a6af4825d2b936030a0a6864c608f1015db30e9f5f9cdb", size = 15212989 },
+ { url = "https://files.pythonhosted.org/packages/99/29/1afb24345ffa6dd351170adc9b30d8a3855c47a2b85f093f28b7366c2a6d/uv-0.5.31-py3-none-musllinux_1_1_i686.whl", hash = "sha256:e6b5a29c29e774525baf982f570c53e8862f19e3f7e74bd819c7b3749f4cdfa0", size = 15554448 },
+ { url = "https://files.pythonhosted.org/packages/5a/5f/784cbe68aa0c291587a3735a61372dc02521780ccd0f0058f159a451df19/uv-0.5.31-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:15109a938c56ee1e1c997b291743812af3ea1d7547b0929569494c359082a993", size = 16405791 },
+ { url = "https://files.pythonhosted.org/packages/9f/80/458b8f67e41dddc3c6ca1515ea8136c217a52b92dedd8c53f9eb00287d22/uv-0.5.31-py3-none-win32.whl", hash = "sha256:f2161ef8b9a0308f05dd4a3eb2c1d104301e23c699fab5898e9fc38387690e4b", size = 15602489 },
+ { url = "https://files.pythonhosted.org/packages/4c/50/f3f89c6bd27aae15ca3150b839c9d8f5d32a9a19a6eae3daa6d9aae1de4f/uv-0.5.31-py3-none-win_amd64.whl", hash = "sha256:bcc57b75883516233658ff1daee0d17347a8b872f717a1644d36e8ea2b021f45", size = 16895932 },
+ { url = "https://files.pythonhosted.org/packages/12/64/af4aa07bc1c525b1fefd1686d31a43a74eac51e74046755ffdca4502784d/uv-0.5.31-py3-none-win_arm64.whl", hash = "sha256:51ceab5a128dd22bcd62489107563e10084e13ed9c15107193c2d7d1139979f4", size = 15776619 },
]
From 8a817d36b335ed949e0a01085883bc51866c9aef Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 1 Mar 2025 16:55:15 -0600
Subject: [PATCH 51/67] Upgrade dependencies and actions runner Upgrade
`libcomponent` to v0.0.3
---
.github/workflows/autodeps.yml | 2 +-
.github/workflows/ci.yml | 4 +-
.pre-commit-config.yaml | 6 +-
pyproject.toml | 10 +--
src/azul/async_clock.py | 106 --------------------------------
src/azul/game.py | 2 +-
tests/test_async_clock.py | 62 -------------------
uv.lock | 108 +++++++++++++++++----------------
8 files changed, 67 insertions(+), 233 deletions(-)
delete mode 100644 src/azul/async_clock.py
delete mode 100644 tests/test_async_clock.py
diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml
index 1862fb9..2974d9b 100644
--- a/.github/workflows/autodeps.yml
+++ b/.github/workflows/autodeps.yml
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout
with:
- persist-credentials: false
+ persist-credentials: true # credentials are needed to push commits
uses: actions/checkout@v4
- name: Setup python
uses: actions/setup-python@v5
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b56f28d..bdc7d48 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python: ['3.10', '3.11', '3.12']
+ python: ['3.10', '3.11', '3.12', '3.13']
arch: ['x86', 'x64']
continue-on-error: >-
${{
@@ -107,7 +107,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python: ['3.10', '3.11', '3.12']
+ python: ['3.10', '3.11', '3.12', '3.13']
continue-on-error: >-
${{
(
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3f9da0e..f002e4b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -24,7 +24,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.9.6
+ rev: v0.9.9
hooks:
- id: ruff
types: [file]
@@ -41,10 +41,10 @@ repos:
additional_dependencies:
- tomli
- repo: https://github.com/crate-ci/typos
- rev: typos-dict-v0.12.5
+ rev: v1.30.0
hooks:
- id: typos
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v1.3.1
+ rev: v1.4.1
hooks:
- id: zizmor
diff --git a/pyproject.toml b/pyproject.toml
index ebdd41e..c3a5cad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,13 +36,13 @@ keywords = [
"ai", "multi-player", "azul", "ai-support", "networked-game"
]
dependencies = [
- "exceptiongroup; python_version < '3.11'",
- "libcomponent~=0.0.1",
+ "exceptiongroup>=1.2.2; python_version < '3.11'",
+ "libcomponent~=0.0.3",
"mypy_extensions>=1.0.0",
"numpy~=2.1.3",
"orjson>=3.10,<4",
"pygame~=2.6.0",
- "trio~=0.28.0",
+ "trio~=0.29.0",
]
[tool.setuptools.dynamic]
@@ -58,8 +58,8 @@ azul_game = "azul.game:cli_run"
[project.optional-dependencies]
tests = [
"pytest>=5.0",
- "pytest-cov",
- "pytest-trio",
+ "pytest-cov>=6.0.0",
+ "pytest-trio>=0.8.0",
"coverage>=7.2.5",
"uv>=0.5.21",
"mypy>=1.14.1",
diff --git a/src/azul/async_clock.py b/src/azul/async_clock.py
deleted file mode 100644
index 9e93365..0000000
--- a/src/azul/async_clock.py
+++ /dev/null
@@ -1,106 +0,0 @@
-"""Asynchronous Clock - Asynchronous version of pygame.time.Clock."""
-
-# Programmed by CoolCat467
-
-from __future__ import annotations
-
-# Copyright (C) 2023 CoolCat467
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-__title__ = "Async Clock"
-__author__ = "CoolCat467"
-__license__ = "GNU General Public License Version 3"
-__version__ = "0.0.0"
-
-
-from time import perf_counter_ns
-from typing import NewType
-
-import trio
-
-nanoseconds = NewType("nanoseconds", int)
-
-
-def get_ticks() -> nanoseconds:
- """Get Ticks."""
- return nanoseconds(perf_counter_ns())
-
-
-class Clock:
- """pygame.time.Clock but with asynchronous tick."""
-
- __slots__ = (
- "fps",
- "fps_count",
- "fps_tick",
- "last_tick",
- "rawpassed",
- "timepassed",
- )
-
- def __init__(self) -> None:
- """Initialize variables."""
- self.fps_tick = nanoseconds(0)
- self.timepassed = nanoseconds(0)
- self.rawpassed = nanoseconds(0)
- self.last_tick: nanoseconds = get_ticks()
- self.fps = 0.0
- self.fps_count = 0
-
- def __repr__(self) -> str:
- """Return representation of self."""
- return f"<{self.__class__.__name__}({self.fps:2f})>"
-
- def get_fps(self) -> float:
- """Return the clock framerate in Frames Per Second."""
- return self.fps
-
- def get_rawtime(self) -> nanoseconds:
- """Return the actual time used in the previous tick in nanoseconds (original was milliseconds)."""
- return self.rawpassed
-
- def get_time(self) -> nanoseconds:
- """Return time used in the previous tick (in nanoseconds, original was milliseconds)."""
- return self.timepassed
-
- async def tick(self, framerate: int = 0) -> int:
- """Tick the clock. Return time passed in nanoseconds, same as get_time (original was milliseconds)."""
- endtime = 1000000000 // framerate if framerate > 0 else 0
- self.rawpassed = nanoseconds(get_ticks() - self.last_tick)
- delay = endtime - self.rawpassed
- if delay > 0:
- await trio.sleep(delay / 1e9) # nanoseconds -> seconds
- else:
- await trio.lowlevel.checkpoint()
-
- nowtime: nanoseconds = get_ticks()
- self.timepassed = nanoseconds(nowtime - self.last_tick)
- self.fps_count += 1
- self.last_tick = nowtime
-
- if not self.fps_tick:
- self.fps_count = 0
- self.fps_tick = nowtime
- if self.fps_count >= 10:
- self.fps = self.fps_count / (
- (nowtime - self.fps_tick) / 1e9
- ) # nanoseconds -> seconds
- self.fps_count = 0
- self.fps_tick = nowtime
- return self.timepassed
-
-
-if __name__ == "__main__": # pragma: nocover
- print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n")
diff --git a/src/azul/game.py b/src/azul/game.py
index df3c1bb..3def590 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -38,6 +38,7 @@
import pygame
import trio
+from libcomponent.async_clock import Clock
from libcomponent.component import (
Component,
ComponentManager,
@@ -60,7 +61,6 @@
from pygame.sprite import LayeredDirty
from azul import database, element_list, objects, sprite
-from azul.async_clock import Clock
from azul.client import GameClient, read_advertisements
from azul.crop import auto_crop_clear
from azul.network_shared import DEFAULT_PORT
diff --git a/tests/test_async_clock.py b/tests/test_async_clock.py
deleted file mode 100644
index fa8256d..0000000
--- a/tests/test_async_clock.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from __future__ import annotations
-
-import pytest
-
-from azul.async_clock import Clock
-
-
-@pytest.fixture
-def clock() -> Clock:
- return Clock()
-
-
-def test_initial_values(clock: Clock) -> None:
- assert clock.fps == 0.0
- assert clock.fps_count == 0
-
-
-def test_get_fps(clock: Clock) -> None:
- assert clock.get_fps() == 0.0
-
-
-def test_get_rawtime(clock: Clock) -> None:
- assert clock.get_rawtime() == 0
-
-
-def test_get_time(clock: Clock) -> None:
- assert clock.get_time() == 0
-
-
-@pytest.mark.trio
-async def test_tick_elapsed(clock: Clock) -> None:
- time_passed = await clock.tick()
- assert time_passed >= 0
-
- # Test with a specific framerate
- time_passed = await clock.tick(60)
- assert time_passed >= int(1e9 // 60)
-
- # Test with a zero framerate
- time_passed = await clock.tick(0)
- assert time_passed >= 0
-
-
-@pytest.mark.trio
-async def test_tick(clock: Clock) -> None:
- await clock.tick(60)
- result = await clock.tick(60)
- assert isinstance(result, int)
- assert result >= int(1e9 // 60)
- assert repr(clock).startswith(" None:
- for _ in range(20):
- await clock.tick(1024)
- fps = clock.get_fps()
- assert isinstance(fps, float)
- assert fps >= 0
diff --git a/uv.lock b/uv.lock
index c3cebe9..22ccfe9 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,4 +1,5 @@
version = 1
+revision = 1
requires-python = ">=3.10"
[[package]]
@@ -43,20 +44,21 @@ requires-dist = [
{ name = "black", marker = "implementation_name == 'cpython' and extra == 'tools'", specifier = ">=24.10.0" },
{ name = "codespell", marker = "extra == 'tools'", specifier = ">=2.3.0" },
{ name = "coverage", marker = "extra == 'tests'", specifier = ">=7.2.5" },
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "libcomponent", specifier = "~=0.0.1" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'", specifier = ">=1.2.2" },
+ { name = "libcomponent", specifier = "~=0.0.3" },
{ name = "mypy", marker = "extra == 'tests'", specifier = ">=1.14.1" },
{ name = "mypy-extensions", specifier = ">=1.0.0" },
{ name = "numpy", specifier = "~=2.1.3" },
{ name = "orjson", specifier = ">=3.10,<4" },
{ name = "pygame", specifier = "~=2.6.0" },
{ name = "pytest", marker = "extra == 'tests'", specifier = ">=5.0" },
- { name = "pytest-cov", marker = "extra == 'tests'" },
- { name = "pytest-trio", marker = "extra == 'tests'" },
+ { name = "pytest-cov", marker = "extra == 'tests'", specifier = ">=6.0.0" },
+ { name = "pytest-trio", marker = "extra == 'tests'", specifier = ">=0.8.0" },
{ name = "ruff", marker = "extra == 'tools'", specifier = ">=0.9.2" },
- { name = "trio", specifier = "~=0.28.0" },
+ { name = "trio", specifier = "~=0.29.0" },
{ name = "uv", marker = "extra == 'tests'", specifier = ">=0.5.21" },
]
+provides-extras = ["tests", "tools"]
[[package]]
name = "black"
@@ -314,7 +316,7 @@ wheels = [
[[package]]
name = "libcomponent"
-version = "0.0.2"
+version = "0.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
@@ -322,9 +324,9 @@ dependencies = [
{ name = "trio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2f/e9/7a72fba139f5d40686c2ca35bb9ce7417b0fa48d14206e5df9997636e5a9/libcomponent-0.0.2.tar.gz", hash = "sha256:cd337db0b435d69e723c8475be0df84f5fbed73756f08090e237fb51a7f197a3", size = 75171 }
+sdist = { url = "https://files.pythonhosted.org/packages/3c/98/edd63ce51271acda4352e70cb1a884915d2066ce733c70e02286b8c23fdb/libcomponent-0.0.3.tar.gz", hash = "sha256:83115c93e5ea51cd17b592ac7ccc25a78f1e4833d1efdc83ae93ca77cbee60da", size = 76329 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/81/ff/1626669a3ece34c6966750ef5623165947eb3c94f35598a309e9bac870f7/libcomponent-0.0.2-py3-none-any.whl", hash = "sha256:1bfd730d2044349005dc796c624b239a9799fcf01272a8f6a0c43372e2751047", size = 57907 },
+ { url = "https://files.pythonhosted.org/packages/23/cc/89d6330283696a804d85568084493d2f01716098ae6548e009e0a2b5663b/libcomponent-0.0.3-py3-none-any.whl", hash = "sha256:5879f87058e1f903bf00f300675f011f4f4627350db72962eb70752acf2dbd92", size = 59324 },
]
[[package]]
@@ -635,27 +637,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.9.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 },
- { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 },
- { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 },
- { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 },
- { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 },
- { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 },
- { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 },
- { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 },
- { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 },
- { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 },
- { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 },
- { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 },
- { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 },
- { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 },
- { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 },
- { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 },
- { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 },
+version = "0.9.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/c3/418441a8170e8d53d05c0b9dad69760dbc7b8a12c10dbe6db1e1205d2377/ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933", size = 3717448 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/c3/2c4afa9ba467555d074b146d9aed0633a56ccdb900839fb008295d037b89/ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367", size = 10027252 },
+ { url = "https://files.pythonhosted.org/packages/33/d1/439e58487cf9eac26378332e25e7d5ade4b800ce1eec7dc2cfc9b0d7ca96/ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7", size = 10840721 },
+ { url = "https://files.pythonhosted.org/packages/50/44/fead822c38281ba0122f1b76b460488a175a9bd48b130650a6fb6dbcbcf9/ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d", size = 10161439 },
+ { url = "https://files.pythonhosted.org/packages/11/ae/d404a2ab8e61ddf6342e09cc6b7f7846cce6b243e45c2007dbe0ca928a5d/ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a", size = 10336264 },
+ { url = "https://files.pythonhosted.org/packages/6a/4e/7c268aa7d84cd709fb6f046b8972313142cffb40dfff1d2515c5e6288d54/ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe", size = 9908774 },
+ { url = "https://files.pythonhosted.org/packages/cc/26/c618a878367ef1b76270fd027ca93692657d3f6122b84ba48911ef5f2edc/ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c", size = 11428127 },
+ { url = "https://files.pythonhosted.org/packages/d7/9a/c5588a93d9bfed29f565baf193fe802fa676a0c837938137ea6cf0576d8c/ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be", size = 12133187 },
+ { url = "https://files.pythonhosted.org/packages/3e/ff/e7980a7704a60905ed7e156a8d73f604c846d9bd87deda9cabfa6cba073a/ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590", size = 11602937 },
+ { url = "https://files.pythonhosted.org/packages/24/78/3690444ad9e3cab5c11abe56554c35f005b51d1d118b429765249095269f/ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb", size = 13771698 },
+ { url = "https://files.pythonhosted.org/packages/6e/bf/e477c2faf86abe3988e0b5fd22a7f3520e820b2ee335131aca2e16120038/ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0", size = 11249026 },
+ { url = "https://files.pythonhosted.org/packages/f7/82/cdaffd59e5a8cb5b14c408c73d7a555a577cf6645faaf83e52fe99521715/ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17", size = 10220432 },
+ { url = "https://files.pythonhosted.org/packages/fe/a4/2507d0026225efa5d4412b6e294dfe54725a78652a5c7e29e6bd0fc492f3/ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1", size = 9874602 },
+ { url = "https://files.pythonhosted.org/packages/d5/be/f3aab1813846b476c4bcffe052d232244979c3cd99d751c17afb530ca8e4/ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57", size = 10851212 },
+ { url = "https://files.pythonhosted.org/packages/8b/45/8e5fd559bea0d2f57c4e12bf197a2fade2fac465aa518284f157dfbca92b/ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e", size = 11327490 },
+ { url = "https://files.pythonhosted.org/packages/42/55/e6c90f13880aeef327746052907e7e930681f26a164fe130ddac28b08269/ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1", size = 10227912 },
+ { url = "https://files.pythonhosted.org/packages/35/b2/da925693cb82a1208aa34966c0f36cb222baca94e729dd22a587bc22d0f3/ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1", size = 11355632 },
+ { url = "https://files.pythonhosted.org/packages/31/d8/de873d1c1b020d668d8ec9855d390764cb90cf8f6486c0983da52be8b7b7/ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf", size = 10435860 },
]
[[package]]
@@ -717,7 +719,7 @@ wheels = [
[[package]]
name = "trio"
-version = "0.28.0"
+version = "0.29.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
@@ -728,9 +730,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "sortedcontainers" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b3/73/57efab729506a8d4b89814f1e356ec8f3369de0ed4fd7e7616974d09646d/trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05", size = 580318 }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b4/04/9954a59e1fb6732f5436225c9af963811d7b24ea62a8bf96991f2cb8c26e/trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94", size = 486317 },
+ { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 },
]
[[package]]
@@ -744,25 +746,25 @@ wheels = [
[[package]]
name = "uv"
-version = "0.5.31"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/67/04/63265828848c2ca60e322408ed529587f670ee97c5607114df08c389398a/uv-0.5.31.tar.gz", hash = "sha256:59c4c6e3704208a8dd5e8d51b79ec995db18a64bd3ff88fd239ca433fbaf1694", size = 2875508 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/40/80/58942f09e0a38fdef37d54d553175e3c195da32547711c78dcc70876f2ce/uv-0.5.31-py3-none-linux_armv6l.whl", hash = "sha256:ba5707a6e363284ba1acd29ae9e70e2377ed31e272b953069798c444bae847ef", size = 15475386 },
- { url = "https://files.pythonhosted.org/packages/a2/ed/1605df7bd74eac86975a48e16a76ae04feedc9d27dc841e8d4f3c00a790f/uv-0.5.31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3169a373d0d41571a7b9d4a442f875f6e26250693ced7779f62461f52ba1da64", size = 15608043 },
- { url = "https://files.pythonhosted.org/packages/1f/5a/1eb42f481a9f9010c8c194d70ab375a6eda96d67ca1fd011bf869d4016c8/uv-0.5.31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:335c16f91b46b4f4a3b31c18cf112a0643d59d4c1708a177103621da0addbaef", size = 14523527 },
- { url = "https://files.pythonhosted.org/packages/9a/05/9817ea1f0d8e7134ed60abeefa7bdc602f4a4a6e2ccdf2760b54fb3dcef3/uv-0.5.31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:cedceefebf2123b514464671d0544a8db126071c2d56dbc10d408b8222939e6a", size = 14940294 },
- { url = "https://files.pythonhosted.org/packages/e4/2d/aee8e68026057c6db71424e3a312d739af8838ae35321bfa1f5900e93d1c/uv-0.5.31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7233182a2b8226011562341f05aaee19925b48730fccdb2e7ee20e31a84f12db", size = 15211133 },
- { url = "https://files.pythonhosted.org/packages/58/cf/16c3b71c903e7d8c3aeb0b85efbf2efb4694ffeab72165d7d9166bf2d497/uv-0.5.31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ce4dc079fd5ddf1946e6085b6ece126ce7c4be23ba27e4010aa68fdec004191", size = 15943734 },
- { url = "https://files.pythonhosted.org/packages/7c/28/8421b94710581c81a9240df95f04b87cfffd5da229eb178733acb6d1a6de/uv-0.5.31-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:007576e1b62268d4a21d4a375d43ff5ae3698313a11f7702c8e7cb5bd29d7f1b", size = 16890117 },
- { url = "https://files.pythonhosted.org/packages/be/9c/a3d4318aebbc68158dc069d3f8de423d56ec3a38017401e92e9e37fe5afc/uv-0.5.31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51d8287cdb760ea8c44b374cb96a59fae2292f1b3e18e228f7ed817d2bd96243", size = 16623168 },
- { url = "https://files.pythonhosted.org/packages/dd/b1/32a5e1239eca3915bec3825dab8c635f80c64b09ae46cf03d1bef7641892/uv-0.5.31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27ce8f3eecd281a6ec255644a328b60eb10044e506a46be931db7bbfe8db89ab", size = 20939390 },
- { url = "https://files.pythonhosted.org/packages/ce/2e/0c3ac2f5be92492cbe735de7f66a83b2d3e22bd59554deaa0106562cba45/uv-0.5.31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d07e9db12a55005a28bb49ecfa444a0221702158fc021f79e26d8e174f1ebdf9", size = 16293460 },
- { url = "https://files.pythonhosted.org/packages/cc/de/59e6665d9f1d4fc93c0b3383eaf31dbf7088cf8fce5c239b5eb8f0bf911b/uv-0.5.31-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8acf6bcb0c0c27e1a157926f35dc70b1c7620c1a2e1124ffacdbf21c78265761", size = 15234496 },
- { url = "https://files.pythonhosted.org/packages/32/14/e69d04bc77f73a34d2d850d60cf21ded8cf0f3481302ea31533ad5a64733/uv-0.5.31-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:a8f27ea8441ce9de43a6af4825d2b936030a0a6864c608f1015db30e9f5f9cdb", size = 15212989 },
- { url = "https://files.pythonhosted.org/packages/99/29/1afb24345ffa6dd351170adc9b30d8a3855c47a2b85f093f28b7366c2a6d/uv-0.5.31-py3-none-musllinux_1_1_i686.whl", hash = "sha256:e6b5a29c29e774525baf982f570c53e8862f19e3f7e74bd819c7b3749f4cdfa0", size = 15554448 },
- { url = "https://files.pythonhosted.org/packages/5a/5f/784cbe68aa0c291587a3735a61372dc02521780ccd0f0058f159a451df19/uv-0.5.31-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:15109a938c56ee1e1c997b291743812af3ea1d7547b0929569494c359082a993", size = 16405791 },
- { url = "https://files.pythonhosted.org/packages/9f/80/458b8f67e41dddc3c6ca1515ea8136c217a52b92dedd8c53f9eb00287d22/uv-0.5.31-py3-none-win32.whl", hash = "sha256:f2161ef8b9a0308f05dd4a3eb2c1d104301e23c699fab5898e9fc38387690e4b", size = 15602489 },
- { url = "https://files.pythonhosted.org/packages/4c/50/f3f89c6bd27aae15ca3150b839c9d8f5d32a9a19a6eae3daa6d9aae1de4f/uv-0.5.31-py3-none-win_amd64.whl", hash = "sha256:bcc57b75883516233658ff1daee0d17347a8b872f717a1644d36e8ea2b021f45", size = 16895932 },
- { url = "https://files.pythonhosted.org/packages/12/64/af4aa07bc1c525b1fefd1686d31a43a74eac51e74046755ffdca4502784d/uv-0.5.31-py3-none-win_arm64.whl", hash = "sha256:51ceab5a128dd22bcd62489107563e10084e13ed9c15107193c2d7d1139979f4", size = 15776619 },
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/31/8f354a0b1df7ef4cb42da118dfae046d49f2c57ae427eb948a48a236c37d/uv-0.6.3.tar.gz", hash = "sha256:73587a192f2ebb8a25431d01037fe19f713fa99ff3b9fdf6e7a121131c6c5649", size = 3081857 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/c2/5a4138f1c615c7702943ce94155349943b5813e51faa38b6876a2ab86033/uv-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:facfec798eaddd07615b3a52973e38f2c8862ceb1bc685a5091891cd6c0c2a21", size = 15524019 },
+ { url = "https://files.pythonhosted.org/packages/02/1d/abf01aa5e02b0a066f77b69a4f2f771c2ccd5424cd553e218afb026c65b9/uv-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b261895497f3c55a8a8917db0a1daeba1a9988ba487b068198d6cc4e8c13e769", size = 15537243 },
+ { url = "https://files.pythonhosted.org/packages/ea/ac/4c1d5e04868051874dce74333fbe98e1f61e40a1522a9258a998775f2fab/uv-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08e3f71a39c76c5b9ab63f9341b433a4ab8a1cc4e29d34ce81bd3b6f5bd642d8", size = 14450283 },
+ { url = "https://files.pythonhosted.org/packages/00/8b/6cdb9a8cb4a5579d8b22d632e98d01f7c3695066ce1a2e33036edba2413a/uv-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ebd4d1012c5043fe507f1f4477e7a54ec81e939e2a6e0229f23abb242f1622f5", size = 14909401 },
+ { url = "https://files.pythonhosted.org/packages/51/8e/4d8c31250c7440a4c3704e81dab39f7f75db046e8b23f5322c3e47549557/uv-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f63b659a5ccbbd8c0ca5200c83ada6d19e73c0f1cafb8f4d9a7ef32544beb06d", size = 15245520 },
+ { url = "https://files.pythonhosted.org/packages/4b/29/52976b3f7a79e4293763823e59d4de3b77506a1b9d298df0285be4879026/uv-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c23948f242a6bcbd274fa18387a608a52b21a3dfed18d324641964e305c348e9", size = 15890146 },
+ { url = "https://files.pythonhosted.org/packages/54/38/a3c37aaf02b890d908edfec32e7a9b86e0df819df6443837929e40ac8d7e/uv-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0445ce49229001cec0a0b1240c6135e2252a3b8017ae878b0559411688a3e12a", size = 16817703 },
+ { url = "https://files.pythonhosted.org/packages/df/0b/cd75c692266eb1cdea6764f9fb14d88babfa8d8433c414ac18623777760d/uv-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95ab9e9194046f4fb50daec6293e471fc18b6e1d350dba4f5328d0f19f6ec183", size = 16509829 },
+ { url = "https://files.pythonhosted.org/packages/1c/5c/35747d595bf13f5b495a29ec9bb6212fd2fad7d8c32324a7faaeb6a643d0/uv-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af417925d7af00be949ebcab1bf187540bea235e9454aa2193ffae5b7ecc75cf", size = 20477063 },
+ { url = "https://files.pythonhosted.org/packages/23/c7/4ea3d3f23d24240c54deee0248766c320163eef8b0117310f0be168fe0f0/uv-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed2d4e3c6e041bc8b55f931a58d758220e46e828b983967fbb318a117d879351", size = 16190208 },
+ { url = "https://files.pythonhosted.org/packages/83/f2/96d4981c3490fabc5ba787703951124969f5b6dc8e3166543e7534de2dea/uv-0.6.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a936275590f3091b05c03ad3ce69e2f8a4c964e80ae44ce0cf13cc3b412352f1", size = 15145146 },
+ { url = "https://files.pythonhosted.org/packages/2b/62/1be7fb8b97fd057460b733bbdf30e71e771dcfbfab27b7db552fa4e219e6/uv-0.6.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e842e96b941832cd95cb2fce90c5626b33e477773f425005e9237f8fd9ef5696", size = 15245907 },
+ { url = "https://files.pythonhosted.org/packages/e0/1b/5849046e11f8154567b235fc8097ebb6a0d6416b3ce317300d9b06470481/uv-0.6.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:cd51af332fb0f6362cc44e4cca22c2d12c31dd52352c6259cae0e3570ce79da4", size = 15504955 },
+ { url = "https://files.pythonhosted.org/packages/ec/46/d4fa9bd06f84bb83e452f3f201b058cd13969cb979402ff000c2e4c77a1e/uv-0.6.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:328677a74c7d998b654e4bfd50ba4347d0f3deed85284dbd041004a184353806", size = 16317436 },
+ { url = "https://files.pythonhosted.org/packages/0b/d9/f93e4522cf1de51ff1a985ead75df85523cd1b689128b1b033c9e31204b8/uv-0.6.3-py3-none-win32.whl", hash = "sha256:dc2d965481bba716a0cf9d0f81896a70c341a854f0e4273f1887f22e52e5c9fb", size = 15545377 },
+ { url = "https://files.pythonhosted.org/packages/91/ea/27dd790ec0d1f8c4ced06e27a409522bd157ed295a1140b3fb6cac3cd39a/uv-0.6.3-py3-none-win_amd64.whl", hash = "sha256:8fc19471fd4cfde1b31a47c239591d7c6dc0a31213f206d3953c528f9f3b406c", size = 16860609 },
+ { url = "https://files.pythonhosted.org/packages/97/0f/01e48493264d75cfac6c953809e11c8356c77fb6be32dfce831bcf481ab2/uv-0.6.3-py3-none-win_arm64.whl", hash = "sha256:94a9d59c05f22829388e51a62a9cfddef4000a112e1c561bb5bd5761d4d672f1", size = 15697009 },
]
From dcc83d13379e178ff38347a3f10e9c4205f117d4 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 1 Mar 2025 17:08:33 -0600
Subject: [PATCH 52/67] Fix new type issues
---
src/azul/client.py | 4 +++-
src/azul/server.py | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index f604ee1..e10e70e 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -96,7 +96,9 @@ async def read_advertisements(
# trio.socket.inet_aton(network_adapter),
# ),
# )
- group_bin = trio.socket.inet_pton(addrinfo[0], addrinfo[4][0])
+ addr_port = addrinfo[4][0]
+ assert isinstance(addr_port, str)
+ group_bin = trio.socket.inet_pton(addrinfo[0], addr_port)
# Join group
if addrinfo[0] == trio.socket.AF_INET: # IPv4
mreq = group_bin + struct.pack("=I", trio.socket.INADDR_ANY)
diff --git a/src/azul/server.py b/src/azul/server.py
index ef9f7f4..9b3697e 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -491,7 +491,7 @@ async def stop_server(self, event: Event[None] | None = None) -> None:
async def post_advertisement(
self,
udp_socket: trio.socket.SocketType,
- send_to_ip: str,
+ send_to_ip: str | int,
hosting_port: int,
) -> None:
"""Post server advertisement packet."""
From 5d627686b511c18f9d14eb45a69c0898ccafa69a Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Tue, 8 Apr 2025 00:25:40 -0500
Subject: [PATCH 53/67] Switch to https://github.com/adhtruong/mirrors-typos
See https://github.com/crate-ci/typos/issues/390
---
.pre-commit-config.yaml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f002e4b..8bf2618 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -24,7 +24,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.9.9
+ rev: v0.11.4
hooks:
- id: ruff
types: [file]
@@ -40,11 +40,11 @@ repos:
- id: codespell
additional_dependencies:
- tomli
- - repo: https://github.com/crate-ci/typos
- rev: v1.30.0
+ - repo: https://github.com/adhtruong/mirrors-typos
+ rev: v1.31.1
hooks:
- id: typos
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v1.4.1
+ rev: v1.5.2
hooks:
- id: zizmor
From 871a85504495edabf94dbeb1e73f7405f5b08147 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Tue, 8 Apr 2025 00:26:42 -0500
Subject: [PATCH 54/67] Ruff fixes
---
computer_players/machine_client.py | 4 ++--
computer_players/minimax.py | 7 +++----
src/azul/game.py | 7 ++++---
src/azul/sprite.py | 26 +++++++++++++-------------
src/azul/state.py | 2 +-
tests/test_sprite.py | 2 +-
6 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py
index 5a616f1..69fa89d 100644
--- a/computer_players/machine_client.py
+++ b/computer_players/machine_client.py
@@ -174,9 +174,9 @@ async def preform_action(self, action: Action) -> None:
raw_source, raw_dest = action
if isinstance(raw_source, SelectableSourceTiles):
source = raw_source
- dest = cast(tuple[SelectableDestinationTiles, ...], raw_dest)
+ dest = cast("tuple[SelectableDestinationTiles, ...]", raw_dest)
else:
- dest = cast(tuple[SelectableDestinationTiles, ...], action)
+ dest = cast("tuple[SelectableDestinationTiles, ...]", action)
else:
dest = action
diff --git a/computer_players/minimax.py b/computer_players/minimax.py
index 10f9568..4c46356 100644
--- a/computer_players/minimax.py
+++ b/computer_players/minimax.py
@@ -11,13 +11,12 @@
import operator
import random
from abc import ABC, abstractmethod
-from collections.abc import Callable
from enum import IntEnum, auto
from math import inf as infinity
from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar, cast
if TYPE_CHECKING:
- from collections.abc import Iterable
+ from collections.abc import Callable, Iterable
class Player(IntEnum):
@@ -117,7 +116,7 @@ def minimax(
best = min
elif current_player == Player.CHANCE:
value = 0
- best = cast(Callable[[float, float], float], sum)
+ best = cast("Callable[[float, float], float]", sum)
else:
raise ValueError(f"Unexpected player type {current_player!r}")
@@ -173,7 +172,7 @@ def alphabeta(
set_idx = 1
elif current_player == Player.CHANCE:
value = 0
- best = cast(Callable[[float, float], float], sum)
+ best = cast("Callable[[float, float], float]", sum)
else:
raise ValueError(f"Unexpected player type {current_player!r}")
diff --git a/src/azul/game.py b/src/azul/game.py
index 3def590..7339b19 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -897,9 +897,10 @@ def __init__(
self.add_component(sprite.DragClickEventComponent())
self.rows_id = rows_id
- self.rows: dict[int, tuple[int, int]] = {
- i: (Tile.blank, 0) for i in range(5)
- }
+ self.rows: dict[int, tuple[int, int]] = dict.fromkeys(
+ range(5),
+ (Tile.blank, 0),
+ )
self.update_image()
self.visible = True
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index f753ddb..7d9dd20 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -262,14 +262,14 @@ def get_mask(self, identifier: int | str) -> Mask:
def set_image(self, identifier: int | str) -> None:
"""Set sprite component's image by identifier."""
- outline = cast(OutlineComponent, self.get_component("outline"))
+ outline = cast("OutlineComponent", self.get_component("outline"))
if outline.active and outline.mod not in str(identifier):
identifier = outline.get_outline(identifier)
if identifier == self.set_surface:
return
- sprite = cast(Sprite, self.manager.get_component("sprite"))
+ sprite = cast("Sprite", self.manager.get_component("sprite"))
sprite.image = self.get_image(identifier)
sprite.mask = self.get_mask(identifier)
@@ -308,7 +308,7 @@ def active(self) -> bool:
def set_color(self, color: Color | None) -> None:
"""Set color. If None, disable, otherwise enable."""
- manager = cast(ImageComponent, self.manager)
+ manager = cast("ImageComponent", self.manager)
prev = self.active
self.__active = color is not None
if color is None:
@@ -332,7 +332,7 @@ def get_outline_descriptor(self, identifier: str | int) -> str:
def save_outline(self, identifier: str | int) -> None:
"""Save outlined version of given identifier image."""
- manager = cast(ImageComponent, self.manager)
+ manager = cast("ImageComponent", self.manager)
outlined = self.get_outline_descriptor(identifier)
if manager.image_exists(outlined):
@@ -385,7 +385,7 @@ def precalculate_all_outlined(
color: Color | tuple[int, int, int],
) -> None:
"""Precalculate all images outlined."""
- manager = cast(ImageComponent, self.manager)
+ manager = cast("ImageComponent", self.manager)
for image in manager.list_images():
self.precalculate_outline(image, color)
@@ -411,7 +411,7 @@ def __init__(self) -> None:
super().__init__("animation")
def default() -> Iterator[int | str | None]:
- manager = cast(ImageComponent, self.manager)
+ manager = cast("ImageComponent", self.manager)
while True:
yield manager.set_surface
@@ -440,7 +440,7 @@ async def tick(self, tick_event: Event[TickEventData]) -> None:
for _ in range(int(updates)):
new = self.fetch_controller_new_state()
if new is not None:
- manager = cast(ImageComponent, self.manager)
+ manager = cast("ImageComponent", self.manager)
manager.set_image(new)
def bind_handlers(self) -> None:
@@ -479,7 +479,7 @@ def __init__(
def point_toward(self, position: Iterable[int | float]) -> None:
"""Change self.heading to point toward a given position."""
- sprite = cast(Sprite, self.get_component("sprite"))
+ sprite = cast("Sprite", self.get_component("sprite"))
self.heading = Vector2.from_points(
sprite.location,
position,
@@ -487,7 +487,7 @@ def point_toward(self, position: Iterable[int | float]) -> None:
def move_heading_distance(self, distance: float) -> None:
"""Move distance in heading direction."""
- sprite = cast(Sprite, self.get_component("sprite"))
+ sprite = cast("Sprite", self.get_component("sprite"))
change = self.heading * distance
if change:
sprite.location += change
@@ -527,7 +527,7 @@ def __init__(self, event_raise_name: str = "reached_destination") -> None:
def update_heading(self) -> None:
"""Update the heading of the movement component."""
- movement = cast(MovementComponent, self.get_component("movement"))
+ movement = cast("MovementComponent", self.get_component("movement"))
to_dest = self.to_destination()
# If magnitude is zero
if to_dest @ to_dest == 0:
@@ -553,7 +553,7 @@ def __get_destination(self) -> Vector2:
def to_destination(self) -> Vector2:
"""Return vector of self.location to self.destination."""
- sprite = cast(Sprite, self.get_component("sprite"))
+ sprite = cast("Sprite", self.get_component("sprite"))
return Vector2.from_points(sprite.location, self.destination)
async def move_destination_time(self, time_passed: float) -> None:
@@ -563,7 +563,7 @@ async def move_destination_time(self, time_passed: float) -> None:
return
sprite, movement = cast(
- tuple[Sprite, MovementComponent],
+ "tuple[Sprite, MovementComponent]",
self.get_components(("sprite", "movement")),
)
@@ -644,7 +644,7 @@ async def press_start(
if not self.manager_exists:
return
- sprite = cast(Sprite, self.get_component("sprite"))
+ sprite = cast("Sprite", self.get_component("sprite"))
pos = event.data["pos"]
button = event.data["button"]
diff --git a/src/azul/state.py b/src/azul/state.py
index 1182aea..6514ac0 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -99,7 +99,7 @@ def generate_bag_contents() -> Counter[int]:
tile_types = 5
tile_count = 100
count_each = tile_count // tile_types
- return Counter({type_: count_each for type_ in range(tile_types)})
+ return Counter(dict.fromkeys(range(tile_types), count_each))
def bag_draw_tile(bag: Counter[int]) -> int:
diff --git a/tests/test_sprite.py b/tests/test_sprite.py
index 9c7ce9a..1344fc2 100644
--- a/tests/test_sprite.py
+++ b/tests/test_sprite.py
@@ -232,7 +232,7 @@ def test_image_component_set_image_affects_sprite(
image_component: ImageComponent,
) -> None:
image = Surface((1, 1))
- sprite = cast(Sprite, image_component.manager.get_component("sprite"))
+ sprite = cast("Sprite", image_component.manager.get_component("sprite"))
image_component.add_image("test_image", image)
assert sprite.image is None
image_component.set_image("test_image")
From 7936da3f19327e01a5ae89936cb8c02008181cf9 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Tue, 15 Apr 2025 22:13:30 -0500
Subject: [PATCH 55/67] Upgrade dependencies, enable more mypy checks, add
licence heder to `namedtuple_mod`
---
.pre-commit-config.yaml | 2 +-
pyproject.toml | 1 +
src/azul/namedtuple_mod.py | 12 +-
src/azul/vector.py | 17 +-
uv.lock | 416 +++++++++++++++++++------------------
5 files changed, 240 insertions(+), 208 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8bf2618..8175a35 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -24,7 +24,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.11.4
+ rev: v0.11.5
hooks:
- id: ruff
types: [file]
diff --git a/pyproject.toml b/pyproject.toml
index c3a5cad..5ffacb8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -79,6 +79,7 @@ package = true
[tool.mypy]
plugins = ["numpy.typing.mypy_plugin"]
files = ["src/azul/", "computer_players"]
+enable_error_code = ["truthy-bool", "mutable-override"]
show_column_numbers = true
show_error_codes = true
show_traceback = true
diff --git a/src/azul/namedtuple_mod.py b/src/azul/namedtuple_mod.py
index 1832697..21ba1e2 100644
--- a/src/azul/namedtuple_mod.py
+++ b/src/azul/namedtuple_mod.py
@@ -1,4 +1,14 @@
-"""typing.NamedTupleMeta mod."""
+"""typing.NamedTupleMeta modification.
+
+Removes the requirement that NamedTuple can only inherit from
+NamedTuple or Generic
+
+Licensed under the Python Software Foundation License
+(see https://github.com/python/cpython/blob/main/LICENSE)
+
+Original source that this is a modified portion of:
+https://github.com/python/cpython/blob/main/Lib/typing.py
+"""
from __future__ import annotations
diff --git a/src/azul/vector.py b/src/azul/vector.py
index f9aa830..60fb30e 100644
--- a/src/azul/vector.py
+++ b/src/azul/vector.py
@@ -23,12 +23,13 @@
__title__ = "Vector Module"
__author__ = "CoolCat467"
__license__ = "GNU General Public License Version 3"
-__version__ = "2.0.0"
+__version__ = "2.0.1"
import math
import sys
from typing import (
TYPE_CHECKING,
+ ClassVar,
)
from azul.namedtuple_mod import NamedTupleMeta
@@ -66,11 +67,20 @@ class BaseVector:
__slots__ = ()
if TYPE_CHECKING:
+ # Because of type hacks later on, pretend we have
+ # the same things NamedTuple does
+ _field_defaults: ClassVar[dict[str, float]]
+ _fields: ClassVar[tuple[str, ...]]
# D105 is 'Missing docstring in magic method', but this is to handle
# typing issues
def __iter__(self) -> Iterator[float]: ... # noqa: D105
def __getitem__(self, value: int) -> float: ... # noqa: D105
+ def _asdict(self) -> dict[str, float]: ...
+ def _replace(self, /, **kwds: int | float) -> Self: ...
+ def __getnewargs__(self) -> tuple[float, ...]: ... # noqa: D105
+ @classmethod
+ def _make(cls, iterable: Iterable[float]) -> Self: ...
@classmethod
def from_iter(cls: type[Self], iterable: Iterable[float]) -> Self:
@@ -99,6 +109,10 @@ def normalized(self: Self) -> Self:
"""Return a normalized (unit) vector."""
return self / self.magnitude()
+ def __bool__(self: Self) -> bool:
+ """Return if any component is nonzero."""
+ return any(self)
+
# rhs is Right Hand Side
def __add__(
self: Self,
@@ -218,6 +232,7 @@ def clamp(self: Self, min_value: float, max_value: float) -> Self:
if TYPE_CHECKING:
VectorBase = BaseVector
else:
+ # In reality, it's a NamedTuple metaclass
VectorBase = type.__new__(
NamedTupleMeta,
"VectorBase",
diff --git a/uv.lock b/uv.lock
index 22ccfe9..3d45437 100644
--- a/uv.lock
+++ b/uv.lock
@@ -4,11 +4,11 @@ requires-python = ">=3.10"
[[package]]
name = "attrs"
-version = "25.1.0"
+version = "25.3.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 },
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
]
[[package]]
@@ -183,62 +183,62 @@ wheels = [
[[package]]
name = "coverage"
-version = "7.6.12"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ba/67/81dc41ec8f548c365d04a29f1afd492d3176b372c33e47fa2a45a01dc13a/coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8", size = 208345 },
- { url = "https://files.pythonhosted.org/packages/33/43/17f71676016c8829bde69e24c852fef6bd9ed39f774a245d9ec98f689fa0/coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879", size = 208775 },
- { url = "https://files.pythonhosted.org/packages/86/25/c6ff0775f8960e8c0840845b723eed978d22a3cd9babd2b996e4a7c502c6/coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe", size = 237925 },
- { url = "https://files.pythonhosted.org/packages/b0/3d/5f5bd37046243cb9d15fff2c69e498c2f4fe4f9b42a96018d4579ed3506f/coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674", size = 235835 },
- { url = "https://files.pythonhosted.org/packages/b5/f1/9e6b75531fe33490b910d251b0bf709142e73a40e4e38a3899e6986fe088/coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb", size = 236966 },
- { url = "https://files.pythonhosted.org/packages/4f/bc/aef5a98f9133851bd1aacf130e754063719345d2fb776a117d5a8d516971/coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c", size = 236080 },
- { url = "https://files.pythonhosted.org/packages/eb/d0/56b4ab77f9b12aea4d4c11dc11cdcaa7c29130b837eb610639cf3400c9c3/coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c", size = 234393 },
- { url = "https://files.pythonhosted.org/packages/0d/77/28ef95c5d23fe3dd191a0b7d89c82fea2c2d904aef9315daf7c890e96557/coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e", size = 235536 },
- { url = "https://files.pythonhosted.org/packages/29/62/18791d3632ee3ff3f95bc8599115707d05229c72db9539f208bb878a3d88/coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425", size = 211063 },
- { url = "https://files.pythonhosted.org/packages/fc/57/b3878006cedfd573c963e5c751b8587154eb10a61cc0f47a84f85c88a355/coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa", size = 211955 },
- { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 },
- { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 },
- { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 },
- { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 },
- { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 },
- { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 },
- { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 },
- { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 },
- { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 },
- { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 },
- { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 },
- { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 },
- { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 },
- { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 },
- { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 },
- { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 },
- { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 },
- { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 },
- { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 },
- { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 },
- { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 },
- { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 },
- { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 },
- { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 },
- { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 },
- { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 },
- { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 },
- { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 },
- { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 },
- { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 },
- { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 },
- { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 },
- { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 },
- { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 },
- { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 },
- { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 },
- { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 },
- { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 },
- { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 },
- { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 },
- { url = "https://files.pythonhosted.org/packages/7a/7f/05818c62c7afe75df11e0233bd670948d68b36cdbf2a339a095bc02624a8/coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf", size = 200558 },
- { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 },
+version = "7.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379 },
+ { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814 },
+ { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937 },
+ { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849 },
+ { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986 },
+ { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896 },
+ { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613 },
+ { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909 },
+ { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948 },
+ { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844 },
+ { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 },
+ { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 },
+ { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 },
+ { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 },
+ { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 },
+ { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 },
+ { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 },
+ { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 },
+ { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 },
+ { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 },
+ { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 },
+ { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 },
+ { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 },
+ { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 },
+ { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 },
+ { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 },
+ { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 },
+ { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 },
+ { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 },
+ { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 },
+ { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 },
+ { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 },
+ { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 },
+ { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 },
+ { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 },
+ { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 },
+ { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 },
+ { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 },
+ { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 },
+ { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 },
+ { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 },
+ { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 },
+ { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 },
+ { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 },
+ { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 },
+ { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 },
+ { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 },
+ { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 },
+ { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 },
+ { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 },
+ { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 },
+ { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 },
]
[package.optional-dependencies]
@@ -248,43 +248,47 @@ toml = [
[[package]]
name = "cryptography"
-version = "44.0.1"
+version = "44.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022 },
- { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 },
- { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 },
- { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 },
- { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 },
- { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 },
- { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 },
- { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 },
- { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 },
- { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 },
- { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882 },
- { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989 },
- { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714 },
- { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 },
- { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 },
- { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 },
- { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 },
- { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 },
- { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 },
- { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 },
- { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 },
- { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 },
- { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371 },
- { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303 },
- { url = "https://files.pythonhosted.org/packages/15/06/507bfb5c7e048114a0185dd65f7814677a2ba285d15705c3d69e660c21d7/cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183", size = 3380782 },
- { url = "https://files.pythonhosted.org/packages/e0/f1/7fb4982d59aa86e1a116c812b545e7fc045352be07738ae3fb278835a9a4/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", size = 3888155 },
- { url = "https://files.pythonhosted.org/packages/60/7b/cbc203838d3092203493d18b923fbbb1de64e0530b332a713ba376905b0b/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", size = 4106417 },
- { url = "https://files.pythonhosted.org/packages/12/c7/2fe59fb085ab418acc82e91e040a6acaa7b1696fcc1c1055317537fbf0d3/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", size = 3887540 },
- { url = "https://files.pythonhosted.org/packages/48/89/09fc7b115f60f5bd970b80e32244f8e9aeeb9244bf870b63420cec3b5cd5/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", size = 4106040 },
- { url = "https://files.pythonhosted.org/packages/2e/38/3fd83c4690dc7d753a442a284b3826ea5e5c380a411443c66421cd823898/cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", size = 3134657 },
+sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 },
+ { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 },
+ { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 },
+ { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 },
+ { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 },
+ { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 },
+ { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 },
+ { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 },
+ { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 },
+ { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 },
+ { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 },
+ { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 },
+ { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 },
+ { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 },
+ { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 },
+ { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 },
+ { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 },
+ { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 },
+ { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 },
+ { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 },
+ { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 },
+ { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 },
+ { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
+ { url = "https://files.pythonhosted.org/packages/99/10/173be140714d2ebaea8b641ff801cbcb3ef23101a2981cbf08057876f89e/cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", size = 3396886 },
+ { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387 },
+ { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922 },
+ { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715 },
+ { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876 },
+ { url = "https://files.pythonhosted.org/packages/99/ec/6e560908349843718db1a782673f36852952d52a55ab14e46c42c8a7690a/cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", size = 3131719 },
+ { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513 },
+ { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432 },
+ { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421 },
+ { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 },
]
[[package]]
@@ -307,11 +311,11 @@ wheels = [
[[package]]
name = "iniconfig"
-version = "2.0.0"
+version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
]
[[package]]
@@ -440,62 +444,64 @@ wheels = [
[[package]]
name = "orjson"
-version = "3.10.15"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/52/09/e5ff18ad009e6f97eb7edc5f67ef98b3ce0c189da9c3eaca1f9587cd4c61/orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04", size = 249532 },
- { url = "https://files.pythonhosted.org/packages/bd/b8/a75883301fe332bd433d9b0ded7d2bb706ccac679602c3516984f8814fb5/orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8", size = 125229 },
- { url = "https://files.pythonhosted.org/packages/83/4b/22f053e7a364cc9c685be203b1e40fc5f2b3f164a9b2284547504eec682e/orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8", size = 150148 },
- { url = "https://files.pythonhosted.org/packages/63/64/1b54fc75ca328b57dd810541a4035fe48c12a161d466e3cf5b11a8c25649/orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814", size = 139748 },
- { url = "https://files.pythonhosted.org/packages/5e/ff/ff0c5da781807bb0a5acd789d9a7fbcb57f7b0c6e1916595da1f5ce69f3c/orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164", size = 154559 },
- { url = "https://files.pythonhosted.org/packages/4e/9a/11e2974383384ace8495810d4a2ebef5f55aacfc97b333b65e789c9d362d/orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf", size = 130349 },
- { url = "https://files.pythonhosted.org/packages/2d/c4/dd9583aea6aefee1b64d3aed13f51d2aadb014028bc929fe52936ec5091f/orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061", size = 138514 },
- { url = "https://files.pythonhosted.org/packages/53/3e/dcf1729230654f5c5594fc752de1f43dcf67e055ac0d300c8cdb1309269a/orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3", size = 130940 },
- { url = "https://files.pythonhosted.org/packages/e8/2b/b9759fe704789937705c8a56a03f6c03e50dff7df87d65cba9a20fec5282/orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d", size = 414713 },
- { url = "https://files.pythonhosted.org/packages/a7/6b/b9dfdbd4b6e20a59238319eb203ae07c3f6abf07eef909169b7a37ae3bba/orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182", size = 141028 },
- { url = "https://files.pythonhosted.org/packages/7c/b5/40f5bbea619c7caf75eb4d652a9821875a8ed04acc45fe3d3ef054ca69fb/orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e", size = 129715 },
- { url = "https://files.pythonhosted.org/packages/38/60/2272514061cbdf4d672edbca6e59c7e01cd1c706e881427d88f3c3e79761/orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab", size = 142473 },
- { url = "https://files.pythonhosted.org/packages/11/5d/be1490ff7eafe7fef890eb4527cf5bcd8cfd6117f3efe42a3249ec847b60/orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806", size = 133564 },
- { url = "https://files.pythonhosted.org/packages/7a/a2/21b25ce4a2c71dbb90948ee81bd7a42b4fbfc63162e57faf83157d5540ae/orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6", size = 249533 },
- { url = "https://files.pythonhosted.org/packages/b2/85/2076fc12d8225698a51278009726750c9c65c846eda741e77e1761cfef33/orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef", size = 125230 },
- { url = "https://files.pythonhosted.org/packages/06/df/a85a7955f11274191eccf559e8481b2be74a7c6d43075d0a9506aa80284d/orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334", size = 150148 },
- { url = "https://files.pythonhosted.org/packages/37/b3/94c55625a29b8767c0eed194cb000b3787e3c23b4cdd13be17bae6ccbb4b/orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d", size = 139749 },
- { url = "https://files.pythonhosted.org/packages/53/ba/c608b1e719971e8ddac2379f290404c2e914cf8e976369bae3cad88768b1/orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0", size = 154558 },
- { url = "https://files.pythonhosted.org/packages/b2/c4/c1fb835bb23ad788a39aa9ebb8821d51b1c03588d9a9e4ca7de5b354fdd5/orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13", size = 130349 },
- { url = "https://files.pythonhosted.org/packages/78/14/bb2b48b26ab3c570b284eb2157d98c1ef331a8397f6c8bd983b270467f5c/orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5", size = 138513 },
- { url = "https://files.pythonhosted.org/packages/4a/97/d5b353a5fe532e92c46467aa37e637f81af8468aa894cd77d2ec8a12f99e/orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b", size = 130942 },
- { url = "https://files.pythonhosted.org/packages/b5/5d/a067bec55293cca48fea8b9928cfa84c623be0cce8141d47690e64a6ca12/orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399", size = 414717 },
- { url = "https://files.pythonhosted.org/packages/6f/9a/1485b8b05c6b4c4db172c438cf5db5dcfd10e72a9bc23c151a1137e763e0/orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388", size = 141033 },
- { url = "https://files.pythonhosted.org/packages/f8/d2/fc67523656e43a0c7eaeae9007c8b02e86076b15d591e9be11554d3d3138/orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c", size = 129720 },
- { url = "https://files.pythonhosted.org/packages/79/42/f58c7bd4e5b54da2ce2ef0331a39ccbbaa7699b7f70206fbf06737c9ed7d/orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e", size = 142473 },
- { url = "https://files.pythonhosted.org/packages/00/f8/bb60a4644287a544ec81df1699d5b965776bc9848d9029d9f9b3402ac8bb/orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e", size = 133570 },
- { url = "https://files.pythonhosted.org/packages/66/85/22fe737188905a71afcc4bf7cc4c79cd7f5bbe9ed1fe0aac4ce4c33edc30/orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a", size = 249504 },
- { url = "https://files.pythonhosted.org/packages/48/b7/2622b29f3afebe938a0a9037e184660379797d5fd5234e5998345d7a5b43/orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d", size = 125080 },
- { url = "https://files.pythonhosted.org/packages/ce/8f/0b72a48f4403d0b88b2a41450c535b3e8989e8a2d7800659a967efc7c115/orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0", size = 150121 },
- { url = "https://files.pythonhosted.org/packages/06/ec/acb1a20cd49edb2000be5a0404cd43e3c8aad219f376ac8c60b870518c03/orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4", size = 139796 },
- { url = "https://files.pythonhosted.org/packages/33/e1/f7840a2ea852114b23a52a1c0b2bea0a1ea22236efbcdb876402d799c423/orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767", size = 154636 },
- { url = "https://files.pythonhosted.org/packages/fa/da/31543337febd043b8fa80a3b67de627669b88c7b128d9ad4cc2ece005b7a/orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41", size = 130621 },
- { url = "https://files.pythonhosted.org/packages/ed/78/66115dc9afbc22496530d2139f2f4455698be444c7c2475cb48f657cefc9/orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514", size = 138516 },
- { url = "https://files.pythonhosted.org/packages/22/84/cd4f5fb5427ffcf823140957a47503076184cb1ce15bcc1165125c26c46c/orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17", size = 130762 },
- { url = "https://files.pythonhosted.org/packages/93/1f/67596b711ba9f56dd75d73b60089c5c92057f1130bb3a25a0f53fb9a583b/orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b", size = 414700 },
- { url = "https://files.pythonhosted.org/packages/7c/0c/6a3b3271b46443d90efb713c3e4fe83fa8cd71cda0d11a0f69a03f437c6e/orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7", size = 141077 },
- { url = "https://files.pythonhosted.org/packages/3b/9b/33c58e0bfc788995eccd0d525ecd6b84b40d7ed182dd0751cd4c1322ac62/orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a", size = 129898 },
- { url = "https://files.pythonhosted.org/packages/01/c1/d577ecd2e9fa393366a1ea0a9267f6510d86e6c4bb1cdfb9877104cac44c/orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665", size = 142566 },
- { url = "https://files.pythonhosted.org/packages/ed/eb/a85317ee1732d1034b92d56f89f1de4d7bf7904f5c8fb9dcdd5b1c83917f/orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa", size = 133732 },
- { url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399 },
- { url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044 },
- { url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066 },
- { url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737 },
- { url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804 },
- { url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583 },
- { url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465 },
- { url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742 },
- { url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669 },
- { url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043 },
- { url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826 },
- { url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542 },
- { url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444 },
+version = "3.10.16"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/a6/22cb9b03baf167bc2d659c9e74d7580147f36e6a155e633801badfd5a74d/orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8", size = 249179 },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/3e68cc33020a6ebd8f359b8628b69d2132cd84fea68155c33057e502ee51/orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00", size = 138510 },
+ { url = "https://files.pythonhosted.org/packages/dc/12/63bee7764ce12052f7c1a1393ce7f26dc392c93081eb8754dd3dce9b7c6b/orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370", size = 132373 },
+ { url = "https://files.pythonhosted.org/packages/b3/d5/2998c2f319adcd572f2b03ba2083e8176863d1055d8d713683ddcf927b71/orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b", size = 136774 },
+ { url = "https://files.pythonhosted.org/packages/00/03/88c236ae307bd0604623204d4a835e15fbf9c75b8535c8f13ef45abd413f/orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06", size = 138030 },
+ { url = "https://files.pythonhosted.org/packages/66/ba/3e256ddfeb364f98fd6ac65774844090d356158b2d1de8998db2bf984503/orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c", size = 142677 },
+ { url = "https://files.pythonhosted.org/packages/2c/71/73a1214bd27baa2ea5184fff4aa6193a114dfb0aa5663dad48fe63e8cd29/orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15", size = 132798 },
+ { url = "https://files.pythonhosted.org/packages/53/ac/0b2f41c0a1e8c095439d0fab3b33103cf41a39be8e6aa2c56298a6034259/orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da", size = 135450 },
+ { url = "https://files.pythonhosted.org/packages/d9/ca/7524c7b0bc815d426ca134dab54cad519802287b808a3846b047a5b2b7a3/orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e", size = 412356 },
+ { url = "https://files.pythonhosted.org/packages/05/1d/3ae2367c255276bf16ff7e1b210dd0af18bc8da20c4e4295755fc7de1268/orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4", size = 152769 },
+ { url = "https://files.pythonhosted.org/packages/d3/2d/8eb10b6b1d30bb69c35feb15e5ba5ac82466cf743d562e3e8047540efd2f/orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551", size = 137223 },
+ { url = "https://files.pythonhosted.org/packages/47/42/f043717930cb2de5fbebe47f308f101bed9ec2b3580b1f99c8284b2f5fe8/orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd", size = 141734 },
+ { url = "https://files.pythonhosted.org/packages/67/99/795ad7282b425b9fddcfb8a31bded5dcf84dba78ecb1e7ae716e84e794da/orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055", size = 133779 },
+ { url = "https://files.pythonhosted.org/packages/97/29/43f91a5512b5d2535594438eb41c5357865fd5e64dec745d90a588820c75/orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739", size = 249180 },
+ { url = "https://files.pythonhosted.org/packages/0c/36/2a72d55e266473c19a86d97b7363bb8bf558ab450f75205689a287d5ce61/orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225", size = 138510 },
+ { url = "https://files.pythonhosted.org/packages/bb/ad/f86d6f55c1a68b57ff6ea7966bce5f4e5163f2e526ddb7db9fc3c2c8d1c4/orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741", size = 132373 },
+ { url = "https://files.pythonhosted.org/packages/5e/8b/d18f2711493a809f3082a88fda89342bc8e16767743b909cd3c34989fba3/orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53", size = 136773 },
+ { url = "https://files.pythonhosted.org/packages/a1/dc/ce025f002f8e0749e3f057c4d773a4d4de32b7b4c1fc5a50b429e7532586/orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14", size = 138029 },
+ { url = "https://files.pythonhosted.org/packages/0e/1b/cf9df85852b91160029d9f26014230366a2b4deb8cc51fabe68e250a8c1a/orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c", size = 142677 },
+ { url = "https://files.pythonhosted.org/packages/92/18/5b1e1e995bffad49dc4311a0bdfd874bc6f135fd20f0e1f671adc2c9910e/orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca", size = 132800 },
+ { url = "https://files.pythonhosted.org/packages/d6/eb/467f25b580e942fcca1344adef40633b7f05ac44a65a63fc913f9a805d58/orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50", size = 135451 },
+ { url = "https://files.pythonhosted.org/packages/8d/4b/9d10888038975cb375982e9339d9495bac382d5c976c500b8d6f2c8e2e4e/orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1", size = 412358 },
+ { url = "https://files.pythonhosted.org/packages/3b/e2/cfbcfcc4fbe619e0ca9bdbbfccb2d62b540bbfe41e0ee77d44a628594f59/orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d", size = 152772 },
+ { url = "https://files.pythonhosted.org/packages/b9/d6/627a1b00569be46173007c11dde3da4618c9bfe18409325b0e3e2a82fe29/orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164", size = 137225 },
+ { url = "https://files.pythonhosted.org/packages/0a/7b/a73c67b505021af845b9f05c7c848793258ea141fa2058b52dd9b067c2b4/orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619", size = 141733 },
+ { url = "https://files.pythonhosted.org/packages/f4/22/5e8217c48d68c0adbfb181e749d6a733761074e598b083c69a1383d18147/orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60", size = 133784 },
+ { url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325 },
+ { url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621 },
+ { url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270 },
+ { url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346 },
+ { url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845 },
+ { url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078 },
+ { url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712 },
+ { url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136 },
+ { url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258 },
+ { url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326 },
+ { url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800 },
+ { url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516 },
+ { url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759 },
+ { url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944 },
+ { url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289 },
+ { url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640 },
+ { url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286 },
+ { url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307 },
+ { url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739 },
+ { url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076 },
+ { url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643 },
+ { url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168 },
+ { url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271 },
+ { url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444 },
+ { url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737 },
+ { url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482 },
+ { url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714 },
+ { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954 },
]
[[package]]
@@ -530,11 +536,11 @@ wheels = [
[[package]]
name = "platformdirs"
-version = "4.3.6"
+version = "4.3.7"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
+ { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 },
]
[[package]]
@@ -593,7 +599,7 @@ wheels = [
[[package]]
name = "pytest"
-version = "8.3.4"
+version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -603,22 +609,22 @@ dependencies = [
{ name = "pluggy" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
+sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
+ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
]
[[package]]
name = "pytest-cov"
-version = "6.0.0"
+version = "6.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
{ name = "pytest" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 }
+sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
+ { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 },
]
[[package]]
@@ -637,27 +643,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.9.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6f/c3/418441a8170e8d53d05c0b9dad69760dbc7b8a12c10dbe6db1e1205d2377/ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933", size = 3717448 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bc/c3/2c4afa9ba467555d074b146d9aed0633a56ccdb900839fb008295d037b89/ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367", size = 10027252 },
- { url = "https://files.pythonhosted.org/packages/33/d1/439e58487cf9eac26378332e25e7d5ade4b800ce1eec7dc2cfc9b0d7ca96/ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7", size = 10840721 },
- { url = "https://files.pythonhosted.org/packages/50/44/fead822c38281ba0122f1b76b460488a175a9bd48b130650a6fb6dbcbcf9/ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d", size = 10161439 },
- { url = "https://files.pythonhosted.org/packages/11/ae/d404a2ab8e61ddf6342e09cc6b7f7846cce6b243e45c2007dbe0ca928a5d/ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a", size = 10336264 },
- { url = "https://files.pythonhosted.org/packages/6a/4e/7c268aa7d84cd709fb6f046b8972313142cffb40dfff1d2515c5e6288d54/ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe", size = 9908774 },
- { url = "https://files.pythonhosted.org/packages/cc/26/c618a878367ef1b76270fd027ca93692657d3f6122b84ba48911ef5f2edc/ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c", size = 11428127 },
- { url = "https://files.pythonhosted.org/packages/d7/9a/c5588a93d9bfed29f565baf193fe802fa676a0c837938137ea6cf0576d8c/ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be", size = 12133187 },
- { url = "https://files.pythonhosted.org/packages/3e/ff/e7980a7704a60905ed7e156a8d73f604c846d9bd87deda9cabfa6cba073a/ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590", size = 11602937 },
- { url = "https://files.pythonhosted.org/packages/24/78/3690444ad9e3cab5c11abe56554c35f005b51d1d118b429765249095269f/ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb", size = 13771698 },
- { url = "https://files.pythonhosted.org/packages/6e/bf/e477c2faf86abe3988e0b5fd22a7f3520e820b2ee335131aca2e16120038/ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0", size = 11249026 },
- { url = "https://files.pythonhosted.org/packages/f7/82/cdaffd59e5a8cb5b14c408c73d7a555a577cf6645faaf83e52fe99521715/ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17", size = 10220432 },
- { url = "https://files.pythonhosted.org/packages/fe/a4/2507d0026225efa5d4412b6e294dfe54725a78652a5c7e29e6bd0fc492f3/ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1", size = 9874602 },
- { url = "https://files.pythonhosted.org/packages/d5/be/f3aab1813846b476c4bcffe052d232244979c3cd99d751c17afb530ca8e4/ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57", size = 10851212 },
- { url = "https://files.pythonhosted.org/packages/8b/45/8e5fd559bea0d2f57c4e12bf197a2fade2fac465aa518284f157dfbca92b/ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e", size = 11327490 },
- { url = "https://files.pythonhosted.org/packages/42/55/e6c90f13880aeef327746052907e7e930681f26a164fe130ddac28b08269/ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1", size = 10227912 },
- { url = "https://files.pythonhosted.org/packages/35/b2/da925693cb82a1208aa34966c0f36cb222baca94e729dd22a587bc22d0f3/ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1", size = 11355632 },
- { url = "https://files.pythonhosted.org/packages/31/d8/de873d1c1b020d668d8ec9855d390764cb90cf8f6486c0983da52be8b7b7/ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf", size = 10435860 },
+version = "0.11.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 },
+ { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637 },
+ { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012 },
+ { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 },
+ { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 },
+ { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 },
+ { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 },
+ { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 },
+ { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 },
+ { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 },
+ { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 },
+ { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 },
+ { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 },
+ { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 },
+ { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 },
+ { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 },
+ { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 },
]
[[package]]
@@ -737,34 +743,34 @@ wheels = [
[[package]]
name = "typing-extensions"
-version = "4.12.2"
+version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
+ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
]
[[package]]
name = "uv"
-version = "0.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4e/31/8f354a0b1df7ef4cb42da118dfae046d49f2c57ae427eb948a48a236c37d/uv-0.6.3.tar.gz", hash = "sha256:73587a192f2ebb8a25431d01037fe19f713fa99ff3b9fdf6e7a121131c6c5649", size = 3081857 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bb/c2/5a4138f1c615c7702943ce94155349943b5813e51faa38b6876a2ab86033/uv-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:facfec798eaddd07615b3a52973e38f2c8862ceb1bc685a5091891cd6c0c2a21", size = 15524019 },
- { url = "https://files.pythonhosted.org/packages/02/1d/abf01aa5e02b0a066f77b69a4f2f771c2ccd5424cd553e218afb026c65b9/uv-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b261895497f3c55a8a8917db0a1daeba1a9988ba487b068198d6cc4e8c13e769", size = 15537243 },
- { url = "https://files.pythonhosted.org/packages/ea/ac/4c1d5e04868051874dce74333fbe98e1f61e40a1522a9258a998775f2fab/uv-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08e3f71a39c76c5b9ab63f9341b433a4ab8a1cc4e29d34ce81bd3b6f5bd642d8", size = 14450283 },
- { url = "https://files.pythonhosted.org/packages/00/8b/6cdb9a8cb4a5579d8b22d632e98d01f7c3695066ce1a2e33036edba2413a/uv-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ebd4d1012c5043fe507f1f4477e7a54ec81e939e2a6e0229f23abb242f1622f5", size = 14909401 },
- { url = "https://files.pythonhosted.org/packages/51/8e/4d8c31250c7440a4c3704e81dab39f7f75db046e8b23f5322c3e47549557/uv-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f63b659a5ccbbd8c0ca5200c83ada6d19e73c0f1cafb8f4d9a7ef32544beb06d", size = 15245520 },
- { url = "https://files.pythonhosted.org/packages/4b/29/52976b3f7a79e4293763823e59d4de3b77506a1b9d298df0285be4879026/uv-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c23948f242a6bcbd274fa18387a608a52b21a3dfed18d324641964e305c348e9", size = 15890146 },
- { url = "https://files.pythonhosted.org/packages/54/38/a3c37aaf02b890d908edfec32e7a9b86e0df819df6443837929e40ac8d7e/uv-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0445ce49229001cec0a0b1240c6135e2252a3b8017ae878b0559411688a3e12a", size = 16817703 },
- { url = "https://files.pythonhosted.org/packages/df/0b/cd75c692266eb1cdea6764f9fb14d88babfa8d8433c414ac18623777760d/uv-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95ab9e9194046f4fb50daec6293e471fc18b6e1d350dba4f5328d0f19f6ec183", size = 16509829 },
- { url = "https://files.pythonhosted.org/packages/1c/5c/35747d595bf13f5b495a29ec9bb6212fd2fad7d8c32324a7faaeb6a643d0/uv-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af417925d7af00be949ebcab1bf187540bea235e9454aa2193ffae5b7ecc75cf", size = 20477063 },
- { url = "https://files.pythonhosted.org/packages/23/c7/4ea3d3f23d24240c54deee0248766c320163eef8b0117310f0be168fe0f0/uv-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed2d4e3c6e041bc8b55f931a58d758220e46e828b983967fbb318a117d879351", size = 16190208 },
- { url = "https://files.pythonhosted.org/packages/83/f2/96d4981c3490fabc5ba787703951124969f5b6dc8e3166543e7534de2dea/uv-0.6.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a936275590f3091b05c03ad3ce69e2f8a4c964e80ae44ce0cf13cc3b412352f1", size = 15145146 },
- { url = "https://files.pythonhosted.org/packages/2b/62/1be7fb8b97fd057460b733bbdf30e71e771dcfbfab27b7db552fa4e219e6/uv-0.6.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e842e96b941832cd95cb2fce90c5626b33e477773f425005e9237f8fd9ef5696", size = 15245907 },
- { url = "https://files.pythonhosted.org/packages/e0/1b/5849046e11f8154567b235fc8097ebb6a0d6416b3ce317300d9b06470481/uv-0.6.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:cd51af332fb0f6362cc44e4cca22c2d12c31dd52352c6259cae0e3570ce79da4", size = 15504955 },
- { url = "https://files.pythonhosted.org/packages/ec/46/d4fa9bd06f84bb83e452f3f201b058cd13969cb979402ff000c2e4c77a1e/uv-0.6.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:328677a74c7d998b654e4bfd50ba4347d0f3deed85284dbd041004a184353806", size = 16317436 },
- { url = "https://files.pythonhosted.org/packages/0b/d9/f93e4522cf1de51ff1a985ead75df85523cd1b689128b1b033c9e31204b8/uv-0.6.3-py3-none-win32.whl", hash = "sha256:dc2d965481bba716a0cf9d0f81896a70c341a854f0e4273f1887f22e52e5c9fb", size = 15545377 },
- { url = "https://files.pythonhosted.org/packages/91/ea/27dd790ec0d1f8c4ced06e27a409522bd157ed295a1140b3fb6cac3cd39a/uv-0.6.3-py3-none-win_amd64.whl", hash = "sha256:8fc19471fd4cfde1b31a47c239591d7c6dc0a31213f206d3953c528f9f3b406c", size = 16860609 },
- { url = "https://files.pythonhosted.org/packages/97/0f/01e48493264d75cfac6c953809e11c8356c77fb6be32dfce831bcf481ab2/uv-0.6.3-py3-none-win_arm64.whl", hash = "sha256:94a9d59c05f22829388e51a62a9cfddef4000a112e1c561bb5bd5761d4d672f1", size = 15697009 },
+version = "0.6.14"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/eb/07bc000a3c05372448b63c45da98630c532ec4e059d848488c3e774d017a/uv-0.6.14.tar.gz", hash = "sha256:a117466f307d164a74444949cc94ec4328ec880fb489cbaa7df324dab14c5c98", size = 3134567 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/bf/3e87dec7728b249458967f39a301376cb776e559c90261c1dac963686dc3/uv-0.6.14-py3-none-linux_armv6l.whl", hash = "sha256:c775e5d7a80ff43cb88856bbdcd838918d5ac3dc362414317e6bbaeb615fff98", size = 16228143 },
+ { url = "https://files.pythonhosted.org/packages/24/b2/111e1ea40453d93c849f36a67397b51d9b458e6e598c3629ffe76d11b490/uv-0.6.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2578f6f8cdbcc036ffad1043f9f66ade3ac0babf29def6abd9eefd4a7c6621cb", size = 16273279 },
+ { url = "https://files.pythonhosted.org/packages/72/89/e7fc8a047f08234cc26d1e37e5f573887744205d087f8e8e6f3d0feb04ce/uv-0.6.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9fc8fe58871b4fe02a863b05b8b1b25ef1b6c60d4d224e85338f5c2be0ab4f0e", size = 15115451 },
+ { url = "https://files.pythonhosted.org/packages/20/1e/72ac3d1e0805d3b49b0a4de46483489ea1989827440f42b0cfb444cdc67f/uv-0.6.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2fb2cd7f6aae21b81474b0051d30e7ed939a9a71714948c47f58b0e7acdd2a80", size = 15540456 },
+ { url = "https://files.pythonhosted.org/packages/fd/47/5aeb7fb80c673bc28ccf3ab99e376b1cd92eac41af6b9b48c0e38b114c54/uv-0.6.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d6ca3f99c1a6c1c430ae8f451133fb4e8c3a22f661c257425402a5d9430bb797", size = 15979820 },
+ { url = "https://files.pythonhosted.org/packages/1f/44/c3ad856473f2ef5f22c865a73a0a37ee82d11fcca78ae82f5ac895a7023a/uv-0.6.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed41877b679e0a1af9ab65427d829b87a81b499017e59c70756d4ba02ca43fcb", size = 16650494 },
+ { url = "https://files.pythonhosted.org/packages/7a/f6/8a1245530c282d470909db78cf56831693c58b90d9b819e35aa2d85fbbe8/uv-0.6.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe9b4361b1c8055301b715fdd94d94eb512053dc4545fec40d3fe3657f655987", size = 17505028 },
+ { url = "https://files.pythonhosted.org/packages/a5/70/0806268440651e2ad1b3542af42b800e20bb7e43050a9ca78f3d1eb4c660/uv-0.6.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998b67bb1cebbe044fc2c5cb251c29cffc56f62a6d55719d6f4e960461d6edad", size = 17245854 },
+ { url = "https://files.pythonhosted.org/packages/2a/3a/0da9780868626466d8c4977fb02d1b0daa80e6f7504d7b662cae3fb4af3d/uv-0.6.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d433925db6e2ef46047b68962d136ff2ef17a7b5609168615f19e60674232c9", size = 21584756 },
+ { url = "https://files.pythonhosted.org/packages/eb/fd/21a82b78173be1a2ea20f4f55154e7252bd80d21ed60b9bbbc0e2047b8d0/uv-0.6.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36aaeb00a70a10f748e16c7a1fc410862e2ba905806e7e9dfbc3e64596309404", size = 16878847 },
+ { url = "https://files.pythonhosted.org/packages/6c/9a/7c84650ae9fb801ecc848d49dcba201243989d9234fe3ec4a4e935ff21c0/uv-0.6.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:11779beb3bd1f92814bc8d8cd350d5228e8f9198cca2f52138b53030a4061d93", size = 15810089 },
+ { url = "https://files.pythonhosted.org/packages/0b/b3/efcbd3a2d298801109b24feee655bb80fe4178aa6bf68e49664c48b342b2/uv-0.6.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bf1ec103cf9a0850f03935dc6a93cacc680fa2c90c3b41cfc10da311afab8f5b", size = 15962056 },
+ { url = "https://files.pythonhosted.org/packages/3f/53/c92c894cb34e9578c2e6dc195bcd4eb0a140dd57c96a60207d847521a902/uv-0.6.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:955e36c98a438a249e178988d4f13b1bb831eb57264d73c459f171b5afd7b023", size = 16255226 },
+ { url = "https://files.pythonhosted.org/packages/df/eb/38bc37856691d53008bf094d03d9e7ab0c2927523a3901c83e152e7c9915/uv-0.6.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2d534e7dc1299c8b53eb7b4c7575e4f0933673ea8b1275d3f3022f5670e311db", size = 17005225 },
+ { url = "https://files.pythonhosted.org/packages/d8/fe/087d5193603e16bc5f67556d94cf8fa8634785c5863cccdec825f14e9a4c/uv-0.6.14-py3-none-win32.whl", hash = "sha256:7cdf3c8d927b07d4eaffc44809eb57523d449705f10dabbdd6f34f7bdfc7d5fe", size = 16131231 },
+ { url = "https://files.pythonhosted.org/packages/40/17/33c5c1503c35c874932d4a21ec10a55051e3695dba12b7de700bcfad0cca/uv-0.6.14-py3-none-win_amd64.whl", hash = "sha256:012f46bef6909209c4a6749e4019eb755ba762d37d7ceaaf76da9cb4b7f771e9", size = 17628508 },
+ { url = "https://files.pythonhosted.org/packages/77/09/163062d439ddc0d89e527ae0e631abf1f7781b183442d8823c48af368f5d/uv-0.6.14-py3-none-win_arm64.whl", hash = "sha256:7465081b4d0b213d0055ccb48de7fe546b5cf0853c6d3601115760760634f6d8", size = 16387232 },
]
From 730d093c8c44d129a9df7e9c4867e9c569825f25 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Wed, 7 May 2025 14:09:26 -0500
Subject: [PATCH 56/67] Update dependencies
---
.pre-commit-config.yaml | 6 +-
pyproject.toml | 4 +-
uv.lock | 308 ++++++++++++++++++++--------------------
zizmor.yml | 6 +
4 files changed, 168 insertions(+), 156 deletions(-)
create mode 100644 zizmor.yml
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8175a35..bb7ecec 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -24,7 +24,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.11.5
+ rev: v0.11.8
hooks:
- id: ruff
types: [file]
@@ -41,10 +41,10 @@ repos:
additional_dependencies:
- tomli
- repo: https://github.com/adhtruong/mirrors-typos
- rev: v1.31.1
+ rev: v1.32.0
hooks:
- id: typos
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v1.5.2
+ rev: v1.6.0
hooks:
- id: zizmor
diff --git a/pyproject.toml b/pyproject.toml
index 5ffacb8..c15c228 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,12 +37,12 @@ keywords = [
]
dependencies = [
"exceptiongroup>=1.2.2; python_version < '3.11'",
- "libcomponent~=0.0.3",
+ "libcomponent~=0.0.4",
"mypy_extensions>=1.0.0",
"numpy~=2.1.3",
"orjson>=3.10,<4",
"pygame~=2.6.0",
- "trio~=0.29.0",
+ "trio~=0.30.0",
]
[tool.setuptools.dynamic]
diff --git a/uv.lock b/uv.lock
index 3d45437..caf9687 100644
--- a/uv.lock
+++ b/uv.lock
@@ -45,7 +45,7 @@ requires-dist = [
{ name = "codespell", marker = "extra == 'tools'", specifier = ">=2.3.0" },
{ name = "coverage", marker = "extra == 'tests'", specifier = ">=7.2.5" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'", specifier = ">=1.2.2" },
- { name = "libcomponent", specifier = "~=0.0.3" },
+ { name = "libcomponent", specifier = "~=0.0.4" },
{ name = "mypy", marker = "extra == 'tests'", specifier = ">=1.14.1" },
{ name = "mypy-extensions", specifier = ">=1.0.0" },
{ name = "numpy", specifier = "~=2.1.3" },
@@ -55,7 +55,7 @@ requires-dist = [
{ name = "pytest-cov", marker = "extra == 'tests'", specifier = ">=6.0.0" },
{ name = "pytest-trio", marker = "extra == 'tests'", specifier = ">=0.8.0" },
{ name = "ruff", marker = "extra == 'tools'", specifier = ">=0.9.2" },
- { name = "trio", specifier = "~=0.29.0" },
+ { name = "trio", specifier = "~=0.30.0" },
{ name = "uv", marker = "extra == 'tests'", specifier = ">=0.5.21" },
]
provides-extras = ["tests", "tools"]
@@ -248,47 +248,49 @@ toml = [
[[package]]
name = "cryptography"
-version = "44.0.2"
+version = "44.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 },
- { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 },
- { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 },
- { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 },
- { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 },
- { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 },
- { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 },
- { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 },
- { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 },
- { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 },
- { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 },
- { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 },
- { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 },
- { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 },
- { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 },
- { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 },
- { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 },
- { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 },
- { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 },
- { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 },
- { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 },
- { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 },
- { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 },
- { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
- { url = "https://files.pythonhosted.org/packages/99/10/173be140714d2ebaea8b641ff801cbcb3ef23101a2981cbf08057876f89e/cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", size = 3396886 },
- { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387 },
- { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922 },
- { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715 },
- { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876 },
- { url = "https://files.pythonhosted.org/packages/99/ec/6e560908349843718db1a782673f36852952d52a55ab14e46c42c8a7690a/cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", size = 3131719 },
- { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513 },
- { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432 },
- { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421 },
- { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 },
+sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281 },
+ { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305 },
+ { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040 },
+ { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411 },
+ { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263 },
+ { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198 },
+ { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502 },
+ { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173 },
+ { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713 },
+ { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064 },
+ { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887 },
+ { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737 },
+ { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501 },
+ { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307 },
+ { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876 },
+ { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127 },
+ { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164 },
+ { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081 },
+ { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716 },
+ { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398 },
+ { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900 },
+ { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067 },
+ { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467 },
+ { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375 },
+ { url = "https://files.pythonhosted.org/packages/7f/10/abcf7418536df1eaba70e2cfc5c8a0ab07aa7aa02a5cbc6a78b9d8b4f121/cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d", size = 3393192 },
+ { url = "https://files.pythonhosted.org/packages/06/59/ecb3ef380f5891978f92a7f9120e2852b1df6f0a849c277b8ea45b865db2/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8", size = 3898419 },
+ { url = "https://files.pythonhosted.org/packages/bb/d0/35e2313dbb38cf793aa242182ad5bc5ef5c8fd4e5dbdc380b936c7d51169/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4", size = 4117892 },
+ { url = "https://files.pythonhosted.org/packages/dc/c8/31fb6e33b56c2c2100d76de3fd820afaa9d4d0b6aea1ccaf9aaf35dc7ce3/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff", size = 3900855 },
+ { url = "https://files.pythonhosted.org/packages/43/2a/08cc2ec19e77f2a3cfa2337b429676406d4bb78ddd130a05c458e7b91d73/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06", size = 4117619 },
+ { url = "https://files.pythonhosted.org/packages/02/68/fc3d3f84022a75f2ac4b1a1c0e5d6a0c2ea259e14cd4aae3e0e68e56483c/cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9", size = 3136570 },
+ { url = "https://files.pythonhosted.org/packages/8d/4b/c11ad0b6c061902de5223892d680e89c06c7c4d606305eb8de56c5427ae6/cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375", size = 3390230 },
+ { url = "https://files.pythonhosted.org/packages/58/11/0a6bf45d53b9b2290ea3cec30e78b78e6ca29dc101e2e296872a0ffe1335/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647", size = 3895216 },
+ { url = "https://files.pythonhosted.org/packages/0a/27/b28cdeb7270e957f0077a2c2bfad1b38f72f1f6d699679f97b816ca33642/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259", size = 4115044 },
+ { url = "https://files.pythonhosted.org/packages/35/b0/ec4082d3793f03cb248881fecefc26015813199b88f33e3e990a43f79835/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff", size = 3898034 },
+ { url = "https://files.pythonhosted.org/packages/0b/7f/adf62e0b8e8d04d50c9a91282a57628c00c54d4ae75e2b02a223bd1f2613/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5", size = 4114449 },
+ { url = "https://files.pythonhosted.org/packages/87/62/d69eb4a8ee231f4bf733a92caf9da13f1c81a44e874b1d4080c25ecbb723/cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c", size = 3134369 },
]
[[package]]
@@ -320,7 +322,7 @@ wheels = [
[[package]]
name = "libcomponent"
-version = "0.0.3"
+version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
@@ -328,9 +330,9 @@ dependencies = [
{ name = "trio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3c/98/edd63ce51271acda4352e70cb1a884915d2066ce733c70e02286b8c23fdb/libcomponent-0.0.3.tar.gz", hash = "sha256:83115c93e5ea51cd17b592ac7ccc25a78f1e4833d1efdc83ae93ca77cbee60da", size = 76329 }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/0a/5f3d65ed7bb368baa69c067a1ef478a994b7326c6747a45fc834e8443890/libcomponent-0.0.4.tar.gz", hash = "sha256:c1daa699d748be0e0132c4062e0527a89a6a8dff32c291efa9880c0715d49c1b", size = 76346 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/23/cc/89d6330283696a804d85568084493d2f01716098ae6548e009e0a2b5663b/libcomponent-0.0.3-py3-none-any.whl", hash = "sha256:5879f87058e1f903bf00f300675f011f4f4627350db72962eb70752acf2dbd92", size = 59324 },
+ { url = "https://files.pythonhosted.org/packages/c4/ab/c804e77e769a14bace0ff7d536a2c2b9fb08289a0cab7f76840c92a270b6/libcomponent-0.0.4-py3-none-any.whl", hash = "sha256:c26edf821564da1400ede72d1414bc754cd693199d436e3cde7b46b9950d7587", size = 59380 },
]
[[package]]
@@ -373,11 +375,11 @@ wheels = [
[[package]]
name = "mypy-extensions"
-version = "1.0.0"
+version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
]
[[package]]
@@ -444,64 +446,68 @@ wheels = [
[[package]]
name = "orjson"
-version = "3.10.16"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9d/a6/22cb9b03baf167bc2d659c9e74d7580147f36e6a155e633801badfd5a74d/orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8", size = 249179 },
- { url = "https://files.pythonhosted.org/packages/d7/ce/3e68cc33020a6ebd8f359b8628b69d2132cd84fea68155c33057e502ee51/orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00", size = 138510 },
- { url = "https://files.pythonhosted.org/packages/dc/12/63bee7764ce12052f7c1a1393ce7f26dc392c93081eb8754dd3dce9b7c6b/orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370", size = 132373 },
- { url = "https://files.pythonhosted.org/packages/b3/d5/2998c2f319adcd572f2b03ba2083e8176863d1055d8d713683ddcf927b71/orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b", size = 136774 },
- { url = "https://files.pythonhosted.org/packages/00/03/88c236ae307bd0604623204d4a835e15fbf9c75b8535c8f13ef45abd413f/orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06", size = 138030 },
- { url = "https://files.pythonhosted.org/packages/66/ba/3e256ddfeb364f98fd6ac65774844090d356158b2d1de8998db2bf984503/orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c", size = 142677 },
- { url = "https://files.pythonhosted.org/packages/2c/71/73a1214bd27baa2ea5184fff4aa6193a114dfb0aa5663dad48fe63e8cd29/orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15", size = 132798 },
- { url = "https://files.pythonhosted.org/packages/53/ac/0b2f41c0a1e8c095439d0fab3b33103cf41a39be8e6aa2c56298a6034259/orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da", size = 135450 },
- { url = "https://files.pythonhosted.org/packages/d9/ca/7524c7b0bc815d426ca134dab54cad519802287b808a3846b047a5b2b7a3/orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e", size = 412356 },
- { url = "https://files.pythonhosted.org/packages/05/1d/3ae2367c255276bf16ff7e1b210dd0af18bc8da20c4e4295755fc7de1268/orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4", size = 152769 },
- { url = "https://files.pythonhosted.org/packages/d3/2d/8eb10b6b1d30bb69c35feb15e5ba5ac82466cf743d562e3e8047540efd2f/orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551", size = 137223 },
- { url = "https://files.pythonhosted.org/packages/47/42/f043717930cb2de5fbebe47f308f101bed9ec2b3580b1f99c8284b2f5fe8/orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd", size = 141734 },
- { url = "https://files.pythonhosted.org/packages/67/99/795ad7282b425b9fddcfb8a31bded5dcf84dba78ecb1e7ae716e84e794da/orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055", size = 133779 },
- { url = "https://files.pythonhosted.org/packages/97/29/43f91a5512b5d2535594438eb41c5357865fd5e64dec745d90a588820c75/orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739", size = 249180 },
- { url = "https://files.pythonhosted.org/packages/0c/36/2a72d55e266473c19a86d97b7363bb8bf558ab450f75205689a287d5ce61/orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225", size = 138510 },
- { url = "https://files.pythonhosted.org/packages/bb/ad/f86d6f55c1a68b57ff6ea7966bce5f4e5163f2e526ddb7db9fc3c2c8d1c4/orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741", size = 132373 },
- { url = "https://files.pythonhosted.org/packages/5e/8b/d18f2711493a809f3082a88fda89342bc8e16767743b909cd3c34989fba3/orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53", size = 136773 },
- { url = "https://files.pythonhosted.org/packages/a1/dc/ce025f002f8e0749e3f057c4d773a4d4de32b7b4c1fc5a50b429e7532586/orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14", size = 138029 },
- { url = "https://files.pythonhosted.org/packages/0e/1b/cf9df85852b91160029d9f26014230366a2b4deb8cc51fabe68e250a8c1a/orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c", size = 142677 },
- { url = "https://files.pythonhosted.org/packages/92/18/5b1e1e995bffad49dc4311a0bdfd874bc6f135fd20f0e1f671adc2c9910e/orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca", size = 132800 },
- { url = "https://files.pythonhosted.org/packages/d6/eb/467f25b580e942fcca1344adef40633b7f05ac44a65a63fc913f9a805d58/orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50", size = 135451 },
- { url = "https://files.pythonhosted.org/packages/8d/4b/9d10888038975cb375982e9339d9495bac382d5c976c500b8d6f2c8e2e4e/orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1", size = 412358 },
- { url = "https://files.pythonhosted.org/packages/3b/e2/cfbcfcc4fbe619e0ca9bdbbfccb2d62b540bbfe41e0ee77d44a628594f59/orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d", size = 152772 },
- { url = "https://files.pythonhosted.org/packages/b9/d6/627a1b00569be46173007c11dde3da4618c9bfe18409325b0e3e2a82fe29/orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164", size = 137225 },
- { url = "https://files.pythonhosted.org/packages/0a/7b/a73c67b505021af845b9f05c7c848793258ea141fa2058b52dd9b067c2b4/orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619", size = 141733 },
- { url = "https://files.pythonhosted.org/packages/f4/22/5e8217c48d68c0adbfb181e749d6a733761074e598b083c69a1383d18147/orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60", size = 133784 },
- { url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325 },
- { url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621 },
- { url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270 },
- { url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346 },
- { url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845 },
- { url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078 },
- { url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712 },
- { url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136 },
- { url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258 },
- { url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326 },
- { url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800 },
- { url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516 },
- { url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759 },
- { url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944 },
- { url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289 },
- { url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640 },
- { url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286 },
- { url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307 },
- { url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739 },
- { url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076 },
- { url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643 },
- { url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168 },
- { url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271 },
- { url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444 },
- { url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737 },
- { url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482 },
- { url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714 },
- { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954 },
+version = "3.10.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/16/2ceb9fb7bc2b11b1e4a3ea27794256e93dee2309ebe297fd131a778cd150/orjson-3.10.18-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a45e5d68066b408e4bc383b6e4ef05e717c65219a9e1390abc6155a520cac402", size = 248927 },
+ { url = "https://files.pythonhosted.org/packages/3d/e1/d3c0a2bba5b9906badd121da449295062b289236c39c3a7801f92c4682b0/orjson-3.10.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be3b9b143e8b9db05368b13b04c84d37544ec85bb97237b3a923f076265ec89c", size = 136995 },
+ { url = "https://files.pythonhosted.org/packages/d7/51/698dd65e94f153ee5ecb2586c89702c9e9d12f165a63e74eb9ea1299f4e1/orjson-3.10.18-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9b0aa09745e2c9b3bf779b096fa71d1cc2d801a604ef6dd79c8b1bfef52b2f92", size = 132893 },
+ { url = "https://files.pythonhosted.org/packages/b3/e5/155ce5a2c43a85e790fcf8b985400138ce5369f24ee6770378ee6b691036/orjson-3.10.18-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53a245c104d2792e65c8d225158f2b8262749ffe64bc7755b00024757d957a13", size = 137017 },
+ { url = "https://files.pythonhosted.org/packages/46/bb/6141ec3beac3125c0b07375aee01b5124989907d61c72c7636136e4bd03e/orjson-3.10.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9495ab2611b7f8a0a8a505bcb0f0cbdb5469caafe17b0e404c3c746f9900469", size = 138290 },
+ { url = "https://files.pythonhosted.org/packages/77/36/6961eca0b66b7809d33c4ca58c6bd4c23a1b914fb23aba2fa2883f791434/orjson-3.10.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73be1cbcebadeabdbc468f82b087df435843c809cd079a565fb16f0f3b23238f", size = 142828 },
+ { url = "https://files.pythonhosted.org/packages/8b/2f/0c646d5fd689d3be94f4d83fa9435a6c4322c9b8533edbb3cd4bc8c5f69a/orjson-3.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8936ee2679e38903df158037a2f1c108129dee218975122e37847fb1d4ac68", size = 132806 },
+ { url = "https://files.pythonhosted.org/packages/ea/af/65907b40c74ef4c3674ef2bcfa311c695eb934710459841b3c2da212215c/orjson-3.10.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7115fcbc8525c74e4c2b608129bef740198e9a120ae46184dac7683191042056", size = 135005 },
+ { url = "https://files.pythonhosted.org/packages/c7/d1/68bd20ac6a32cd1f1b10d23e7cc58ee1e730e80624e3031d77067d7150fc/orjson-3.10.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:771474ad34c66bc4d1c01f645f150048030694ea5b2709b87d3bda273ffe505d", size = 413418 },
+ { url = "https://files.pythonhosted.org/packages/31/31/c701ec0bcc3e80e5cb6e319c628ef7b768aaa24b0f3b4c599df2eaacfa24/orjson-3.10.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c14047dbbea52886dd87169f21939af5d55143dad22d10db6a7514f058156a8", size = 153288 },
+ { url = "https://files.pythonhosted.org/packages/d9/31/5e1aa99a10893a43cfc58009f9da840990cc8a9ebb75aa452210ba18587e/orjson-3.10.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641481b73baec8db14fdf58f8967e52dc8bda1f2aba3aa5f5c1b07ed6df50b7f", size = 137181 },
+ { url = "https://files.pythonhosted.org/packages/bf/8c/daba0ac1b8690011d9242a0f37235f7d17df6d0ad941021048523b76674e/orjson-3.10.18-cp310-cp310-win32.whl", hash = "sha256:607eb3ae0909d47280c1fc657c4284c34b785bae371d007595633f4b1a2bbe06", size = 142694 },
+ { url = "https://files.pythonhosted.org/packages/16/62/8b687724143286b63e1d0fab3ad4214d54566d80b0ba9d67c26aaf28a2f8/orjson-3.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:8770432524ce0eca50b7efc2a9a5f486ee0113a5fbb4231526d414e6254eba92", size = 134600 },
+ { url = "https://files.pythonhosted.org/packages/97/c7/c54a948ce9a4278794f669a353551ce7db4ffb656c69a6e1f2264d563e50/orjson-3.10.18-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e0a183ac3b8e40471e8d843105da6fbe7c070faab023be3b08188ee3f85719b8", size = 248929 },
+ { url = "https://files.pythonhosted.org/packages/9e/60/a9c674ef1dd8ab22b5b10f9300e7e70444d4e3cda4b8258d6c2488c32143/orjson-3.10.18-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5ef7c164d9174362f85238d0cd4afdeeb89d9e523e4651add6a5d458d6f7d42d", size = 133364 },
+ { url = "https://files.pythonhosted.org/packages/c1/4e/f7d1bdd983082216e414e6d7ef897b0c2957f99c545826c06f371d52337e/orjson-3.10.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd14c5d99cdc7bf93f22b12ec3b294931518aa019e2a147e8aa2f31fd3240f7", size = 136995 },
+ { url = "https://files.pythonhosted.org/packages/17/89/46b9181ba0ea251c9243b0c8ce29ff7c9796fa943806a9c8b02592fce8ea/orjson-3.10.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b672502323b6cd133c4af6b79e3bea36bad2d16bca6c1f645903fce83909a7a", size = 132894 },
+ { url = "https://files.pythonhosted.org/packages/ca/dd/7bce6fcc5b8c21aef59ba3c67f2166f0a1a9b0317dcca4a9d5bd7934ecfd/orjson-3.10.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51f8c63be6e070ec894c629186b1c0fe798662b8687f3d9fdfa5e401c6bd7679", size = 137016 },
+ { url = "https://files.pythonhosted.org/packages/1c/4a/b8aea1c83af805dcd31c1f03c95aabb3e19a016b2a4645dd822c5686e94d/orjson-3.10.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9478ade5313d724e0495d167083c6f3be0dd2f1c9c8a38db9a9e912cdaf947", size = 138290 },
+ { url = "https://files.pythonhosted.org/packages/36/d6/7eb05c85d987b688707f45dcf83c91abc2251e0dd9fb4f7be96514f838b1/orjson-3.10.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:187aefa562300a9d382b4b4eb9694806e5848b0cedf52037bb5c228c61bb66d4", size = 142829 },
+ { url = "https://files.pythonhosted.org/packages/d2/78/ddd3ee7873f2b5f90f016bc04062713d567435c53ecc8783aab3a4d34915/orjson-3.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da552683bc9da222379c7a01779bddd0ad39dd699dd6300abaf43eadee38334", size = 132805 },
+ { url = "https://files.pythonhosted.org/packages/8c/09/c8e047f73d2c5d21ead9c180203e111cddeffc0848d5f0f974e346e21c8e/orjson-3.10.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e450885f7b47a0231979d9c49b567ed1c4e9f69240804621be87c40bc9d3cf17", size = 135008 },
+ { url = "https://files.pythonhosted.org/packages/0c/4b/dccbf5055ef8fb6eda542ab271955fc1f9bf0b941a058490293f8811122b/orjson-3.10.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5e3c9cc2ba324187cd06287ca24f65528f16dfc80add48dc99fa6c836bb3137e", size = 413419 },
+ { url = "https://files.pythonhosted.org/packages/8a/f3/1eac0c5e2d6d6790bd2025ebfbefcbd37f0d097103d76f9b3f9302af5a17/orjson-3.10.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50ce016233ac4bfd843ac5471e232b865271d7d9d44cf9d33773bcd883ce442b", size = 153292 },
+ { url = "https://files.pythonhosted.org/packages/1f/b4/ef0abf64c8f1fabf98791819ab502c2c8c1dc48b786646533a93637d8999/orjson-3.10.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3ceff74a8f7ffde0b2785ca749fc4e80e4315c0fd887561144059fb1c138aa7", size = 137182 },
+ { url = "https://files.pythonhosted.org/packages/a9/a3/6ea878e7b4a0dc5c888d0370d7752dcb23f402747d10e2257478d69b5e63/orjson-3.10.18-cp311-cp311-win32.whl", hash = "sha256:fdba703c722bd868c04702cac4cb8c6b8ff137af2623bc0ddb3b3e6a2c8996c1", size = 142695 },
+ { url = "https://files.pythonhosted.org/packages/79/2a/4048700a3233d562f0e90d5572a849baa18ae4e5ce4c3ba6247e4ece57b0/orjson-3.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:c28082933c71ff4bc6ccc82a454a2bffcef6e1d7379756ca567c772e4fb3278a", size = 134603 },
+ { url = "https://files.pythonhosted.org/packages/03/45/10d934535a4993d27e1c84f1810e79ccf8b1b7418cef12151a22fe9bb1e1/orjson-3.10.18-cp311-cp311-win_arm64.whl", hash = "sha256:a6c7c391beaedd3fa63206e5c2b7b554196f14debf1ec9deb54b5d279b1b46f5", size = 131400 },
+ { url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184 },
+ { url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279 },
+ { url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799 },
+ { url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791 },
+ { url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059 },
+ { url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359 },
+ { url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853 },
+ { url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131 },
+ { url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834 },
+ { url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368 },
+ { url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359 },
+ { url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466 },
+ { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683 },
+ { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754 },
+ { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218 },
+ { url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087 },
+ { url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273 },
+ { url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779 },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811 },
+ { url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018 },
+ { url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368 },
+ { url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840 },
+ { url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135 },
+ { url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810 },
+ { url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491 },
+ { url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277 },
+ { url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367 },
+ { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687 },
+ { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794 },
+ { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186 },
]
[[package]]
@@ -518,11 +524,11 @@ wheels = [
[[package]]
name = "packaging"
-version = "24.2"
+version = "25.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
@@ -643,27 +649,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.11.5"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 },
- { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637 },
- { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012 },
- { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 },
- { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 },
- { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 },
- { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 },
- { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 },
- { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 },
- { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 },
- { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 },
- { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 },
- { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 },
- { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 },
- { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 },
- { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 },
- { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 },
+version = "0.11.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473 },
+ { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862 },
+ { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273 },
+ { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330 },
+ { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223 },
+ { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353 },
+ { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936 },
+ { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083 },
+ { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834 },
+ { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713 },
+ { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182 },
+ { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027 },
+ { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298 },
+ { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884 },
+ { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102 },
+ { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410 },
+ { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129 },
]
[[package]]
@@ -725,7 +731,7 @@ wheels = [
[[package]]
name = "trio"
-version = "0.29.0"
+version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
@@ -736,9 +742,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "sortedcontainers" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 }
+sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 },
+ { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194 },
]
[[package]]
@@ -752,25 +758,25 @@ wheels = [
[[package]]
name = "uv"
-version = "0.6.14"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e5/eb/07bc000a3c05372448b63c45da98630c532ec4e059d848488c3e774d017a/uv-0.6.14.tar.gz", hash = "sha256:a117466f307d164a74444949cc94ec4328ec880fb489cbaa7df324dab14c5c98", size = 3134567 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6b/bf/3e87dec7728b249458967f39a301376cb776e559c90261c1dac963686dc3/uv-0.6.14-py3-none-linux_armv6l.whl", hash = "sha256:c775e5d7a80ff43cb88856bbdcd838918d5ac3dc362414317e6bbaeb615fff98", size = 16228143 },
- { url = "https://files.pythonhosted.org/packages/24/b2/111e1ea40453d93c849f36a67397b51d9b458e6e598c3629ffe76d11b490/uv-0.6.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2578f6f8cdbcc036ffad1043f9f66ade3ac0babf29def6abd9eefd4a7c6621cb", size = 16273279 },
- { url = "https://files.pythonhosted.org/packages/72/89/e7fc8a047f08234cc26d1e37e5f573887744205d087f8e8e6f3d0feb04ce/uv-0.6.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9fc8fe58871b4fe02a863b05b8b1b25ef1b6c60d4d224e85338f5c2be0ab4f0e", size = 15115451 },
- { url = "https://files.pythonhosted.org/packages/20/1e/72ac3d1e0805d3b49b0a4de46483489ea1989827440f42b0cfb444cdc67f/uv-0.6.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2fb2cd7f6aae21b81474b0051d30e7ed939a9a71714948c47f58b0e7acdd2a80", size = 15540456 },
- { url = "https://files.pythonhosted.org/packages/fd/47/5aeb7fb80c673bc28ccf3ab99e376b1cd92eac41af6b9b48c0e38b114c54/uv-0.6.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d6ca3f99c1a6c1c430ae8f451133fb4e8c3a22f661c257425402a5d9430bb797", size = 15979820 },
- { url = "https://files.pythonhosted.org/packages/1f/44/c3ad856473f2ef5f22c865a73a0a37ee82d11fcca78ae82f5ac895a7023a/uv-0.6.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed41877b679e0a1af9ab65427d829b87a81b499017e59c70756d4ba02ca43fcb", size = 16650494 },
- { url = "https://files.pythonhosted.org/packages/7a/f6/8a1245530c282d470909db78cf56831693c58b90d9b819e35aa2d85fbbe8/uv-0.6.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe9b4361b1c8055301b715fdd94d94eb512053dc4545fec40d3fe3657f655987", size = 17505028 },
- { url = "https://files.pythonhosted.org/packages/a5/70/0806268440651e2ad1b3542af42b800e20bb7e43050a9ca78f3d1eb4c660/uv-0.6.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998b67bb1cebbe044fc2c5cb251c29cffc56f62a6d55719d6f4e960461d6edad", size = 17245854 },
- { url = "https://files.pythonhosted.org/packages/2a/3a/0da9780868626466d8c4977fb02d1b0daa80e6f7504d7b662cae3fb4af3d/uv-0.6.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d433925db6e2ef46047b68962d136ff2ef17a7b5609168615f19e60674232c9", size = 21584756 },
- { url = "https://files.pythonhosted.org/packages/eb/fd/21a82b78173be1a2ea20f4f55154e7252bd80d21ed60b9bbbc0e2047b8d0/uv-0.6.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36aaeb00a70a10f748e16c7a1fc410862e2ba905806e7e9dfbc3e64596309404", size = 16878847 },
- { url = "https://files.pythonhosted.org/packages/6c/9a/7c84650ae9fb801ecc848d49dcba201243989d9234fe3ec4a4e935ff21c0/uv-0.6.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:11779beb3bd1f92814bc8d8cd350d5228e8f9198cca2f52138b53030a4061d93", size = 15810089 },
- { url = "https://files.pythonhosted.org/packages/0b/b3/efcbd3a2d298801109b24feee655bb80fe4178aa6bf68e49664c48b342b2/uv-0.6.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bf1ec103cf9a0850f03935dc6a93cacc680fa2c90c3b41cfc10da311afab8f5b", size = 15962056 },
- { url = "https://files.pythonhosted.org/packages/3f/53/c92c894cb34e9578c2e6dc195bcd4eb0a140dd57c96a60207d847521a902/uv-0.6.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:955e36c98a438a249e178988d4f13b1bb831eb57264d73c459f171b5afd7b023", size = 16255226 },
- { url = "https://files.pythonhosted.org/packages/df/eb/38bc37856691d53008bf094d03d9e7ab0c2927523a3901c83e152e7c9915/uv-0.6.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2d534e7dc1299c8b53eb7b4c7575e4f0933673ea8b1275d3f3022f5670e311db", size = 17005225 },
- { url = "https://files.pythonhosted.org/packages/d8/fe/087d5193603e16bc5f67556d94cf8fa8634785c5863cccdec825f14e9a4c/uv-0.6.14-py3-none-win32.whl", hash = "sha256:7cdf3c8d927b07d4eaffc44809eb57523d449705f10dabbdd6f34f7bdfc7d5fe", size = 16131231 },
- { url = "https://files.pythonhosted.org/packages/40/17/33c5c1503c35c874932d4a21ec10a55051e3695dba12b7de700bcfad0cca/uv-0.6.14-py3-none-win_amd64.whl", hash = "sha256:012f46bef6909209c4a6749e4019eb755ba762d37d7ceaaf76da9cb4b7f771e9", size = 17628508 },
- { url = "https://files.pythonhosted.org/packages/77/09/163062d439ddc0d89e527ae0e631abf1f7781b183442d8823c48af368f5d/uv-0.6.14-py3-none-win_arm64.whl", hash = "sha256:7465081b4d0b213d0055ccb48de7fe546b5cf0853c6d3601115760760634f6d8", size = 16387232 },
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/d4/c1104ee4d8a69e4834888cd850eb4f9327c585e5e60da108fda788d3872d/uv-0.7.2.tar.gz", hash = "sha256:45e619bb076916b79df8c5ecc28d1be04d1ccd0b63b080c44ae973b8deb33b25", size = 3293566 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/c3/68291a239dbedc0389fa5ce5b5b6c7c2a54c52bc11e9503276f376faa9e7/uv-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:e1e4394b54bc387f227ca1b2aa0348d35f6455b6168ca1826c1dc5f4fc3e8d20", size = 16590159 },
+ { url = "https://files.pythonhosted.org/packages/6c/ac/3c7e8df1d6bb84a805aa773ea4f6a006682f8241f331c9c359eb5310f042/uv-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c0edb194c35f1f12c75bec4fe2d7d4d09f0c2cec3a16102217a772620ce1d6e6", size = 16753976 },
+ { url = "https://files.pythonhosted.org/packages/42/ca/6a3f3c094794d482e3418f6a46c2753fa4f6ed2fe5b7ecf299db8cfed9ea/uv-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be2e8d033936ba8ed9ccf85eb2d15c7a8db3bb3e9c4960bdf7c3c98034a6dbda", size = 15513631 },
+ { url = "https://files.pythonhosted.org/packages/1e/65/6fae29e0eb884fa1cab89b0fa865d409e0e2bcada8316cd50b4c81e8706c/uv-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a314a94b42bc6014f18c877f723292306b76c10b455c2b385728e1470e661ced", size = 15972100 },
+ { url = "https://files.pythonhosted.org/packages/a6/92/3d8da1efc7f3272ccc65c50cb13abd9e6a32246bb6c258175c68a91d0d80/uv-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4d1652fe3608fa564dbeaeb2465208f691ac04b57f655ebef62e9ec6d37103d", size = 16288666 },
+ { url = "https://files.pythonhosted.org/packages/2c/5e/7d6a788c45d5e2686d01c4886ebb21149892a59bcfa15b66d0646e73aafa/uv-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48c115a3c13c3b29748e325093ee04fd48eaf91145bedc68727f78e6a1c34ab8", size = 17165785 },
+ { url = "https://files.pythonhosted.org/packages/e4/9e/4d0a947ffa4b377c6e34935c23164c7914d7239154d254aa5938db6a7e83/uv-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c388172209ca5a47706666d570a45fef3dd39db9258682e10b2f62ca521f0e91", size = 18014800 },
+ { url = "https://files.pythonhosted.org/packages/c7/31/781288f9f53e1770128f7830841d7d269097ed70a4afa71578d45721bfa2/uv-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c97cc5e8029a8dc0e1fc39f15f746be931345bc0aeae85feceaa1828f0de87", size = 17745484 },
+ { url = "https://files.pythonhosted.org/packages/6d/04/030eec46217225b77ccff1f2808e64074873d86fe445be3784649506e65e/uv-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fa315366ee36ad1f734734f3153e2f334342900061fc0ed18b06f3b9bb2dfe2", size = 22103174 },
+ { url = "https://files.pythonhosted.org/packages/5c/07/9d85d0a9ddd49dbec18bde741ffb33d0c671a153461b094a9c73504e1b92/uv-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7236ec776c559fbc3ae4389b7cd506a2428ad9dd0402ac3d9446200ea3dc45f6", size = 17369922 },
+ { url = "https://files.pythonhosted.org/packages/11/18/cfef0efe3c4ebdd81422f35215bb915fd599fc946b40306186d87e90678b/uv-0.7.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:78ec372b2f5c7ff8a034e16dd04bc579a62561a5eac4b6dfc96af60298a97d31", size = 16209878 },
+ { url = "https://files.pythonhosted.org/packages/31/ed/2ddd7547203ddd368b9ec56b245e09931f868daf2d2b0e29c0b69584466d/uv-0.7.2-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:28fd5d689ae4f8f16533f091a6dd63e1ddf3b7c782003ac8a18584ddb8823cbe", size = 16271878 },
+ { url = "https://files.pythonhosted.org/packages/f0/9c/30a48a9d875b91b486286d1a4ccc081dad130acea0dca683c1786ddd7c84/uv-0.7.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9aaacb143622cd437a446a4b316a546c02403b438cd7fd7556d62f47a9fd0a99", size = 16742005 },
+ { url = "https://files.pythonhosted.org/packages/a5/b3/5550a721a1e8a99117d960f16c05ad8d39aff79a3fc1aadf2ed13da4385f/uv-0.7.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:81b86fff996c302be6aa1c1ac6eb72b97a7277c319e52c0def50d40b1ffaa617", size = 17443927 },
+ { url = "https://files.pythonhosted.org/packages/52/1f/71a7c3e9c79718647fea1e6fe85ccc82d2629cd858b437ae2081190045cc/uv-0.7.2-py3-none-win32.whl", hash = "sha256:19a64c38657c4fbe7c945055755500116fdaac8e121381a5245ea66823f8c500", size = 16869579 },
+ { url = "https://files.pythonhosted.org/packages/44/f0/4424cf64533b7576610f7de5c94183d810743b08e81072a2bb2d98316947/uv-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dc1ee6114c824f5880c584a96b2947a35817fdd3a0b752d1adbd926ae6872d1c", size = 18287842 },
+ { url = "https://files.pythonhosted.org/packages/0a/5c/12ce48cab21fb0f9bde4ea0c19ec2ab88d4aa9a53e148a52cfb9a41578c9/uv-0.7.2-py3-none-win_arm64.whl", hash = "sha256:0445e56d3f9651ad84d5a7f16efabba83bf305b73594f1c1bc0659aeab952040", size = 16929582 },
]
diff --git a/zizmor.yml b/zizmor.yml
new file mode 100644
index 0000000..c359223
--- /dev/null
+++ b/zizmor.yml
@@ -0,0 +1,6 @@
+rules:
+ unpinned-uses:
+ config:
+ policies:
+ # TODO: use the default policies
+ "*": any
From 9e6eb834753ed36489f41ac1e599b0850334b215 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Wed, 9 Jul 2025 13:21:40 -0500
Subject: [PATCH 57/67] Don't use depreciated numpy plugin
---
pyproject.toml | 1 -
1 file changed, 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index c15c228..42327a1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -77,7 +77,6 @@ azul = ["py.typed", "data/*", "lang/*", "fonts/*"]
package = true
[tool.mypy]
-plugins = ["numpy.typing.mypy_plugin"]
files = ["src/azul/", "computer_players"]
enable_error_code = ["truthy-bool", "mutable-override"]
show_column_numbers = true
From 2d0ea586510e7b7dc4fff9b7ec9ffc5aa4e3c50a Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Wed, 9 Jul 2025 20:01:31 -0500
Subject: [PATCH 58/67] Fix bad merge
---
tests/helpers.py | 175 ---------------------------------------
tests/test_encryption.py | 110 ------------------------
2 files changed, 285 deletions(-)
delete mode 100644 tests/helpers.py
delete mode 100644 tests/test_encryption.py
diff --git a/tests/helpers.py b/tests/helpers.py
deleted file mode 100644
index 0739739..0000000
--- a/tests/helpers.py
+++ /dev/null
@@ -1,175 +0,0 @@
-# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-import inspect
-import unittest.mock
-from functools import partial
-from typing import TYPE_CHECKING, Any, Generic, TypeVar
-
-import trio
-from typing_extensions import ParamSpec
-
-if TYPE_CHECKING:
- from collections.abc import Callable, Coroutine
-
-T = TypeVar("T")
-P = ParamSpec("P")
-T_Mock = TypeVar("T_Mock", bound=unittest.mock.Mock)
-
-
-def synchronize(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]:
- """Take an asynchronous function, and return a synchronous alternative.
-
- This is needed because we sometimes want to test asynchronous behavior in a synchronous test function,
- where we can't simply await something. This function uses `trio.run` and generates a wrapper
- around the original asynchronous function, that awaits the result in a blocking synchronous way,
- returning the obtained value.
- """
-
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
- return trio.run(partial(f, *args, **kwargs))
-
- return wrapper
-
-
-class SynchronizedMixin:
- """Class acting as another wrapped object, with all async methods synchronized.
-
- This class needs :attr:`._WRAPPED_ATTRIBUTE` class variable to be set as the name of the internally
- held attribute, holding the object we'll be wrapping around.
-
- Child classes of this mixin will have their lookup logic changed, to instead perform a lookup
- on the wrapped attribute. Only if that lookup fails, we fallback to this class, meaning if both
- the wrapped attribute and this class have some attribute defined, the attribute from the wrapped
- object is returned. The only exceptions to this are lookup of the ``_WRAPPED_ATTRIBUTE`` variable,
- and of the attribute name stored under the ``_WRAPPED_ATTRIBUTE`` (the wrapped object).
-
- If the attribute held by the wrapped object is an asynchronous function, instead of returning it
- directly, the :func:`.synchronize` function will be called, returning a wrapped synchronous
- alternative for the requested async function.
-
- This is useful when we need to quickly create a synchronous alternative to a class holding async methods.
- However it isn't useful in production, since will cause typing issues (attributes will be accessible, but
- type checkers won't know that they exist here, because of the dynamic nature of this implementation).
- """
-
- _WRAPPED_ATTRIBUTE: str
-
- def __getattribute__(self, name: str, /) -> Any:
- """Return attributes of the wrapped object, if the attribute is a coroutine function, synchronize it.
-
- The only exception to this behavior is getting the :attr:`._WRAPPED_ATTRIBUTE` variable itself, or the
- attribute named as the content of the ``_WRAPPED_ATTRIBUTE`` variable. All other attribute access will
- be delegated to the wrapped attribute. If the wrapped object doesn't have given attribute, the lookup
- will fallback to regular lookup for variables belonging to this class.
- """
- if (
- name == "_WRAPPED_ATTRIBUTE" or name == self._WRAPPED_ATTRIBUTE
- ): # Order is important
- return super().__getattribute__(name)
-
- wrapped = getattr(self, self._WRAPPED_ATTRIBUTE)
-
- if hasattr(wrapped, name):
- obj = getattr(wrapped, name)
- if inspect.iscoroutinefunction(obj):
- return synchronize(obj)
- return obj
-
- return super().__getattribute__(name)
-
- def __setattr__(self, name: str, value: object, /) -> None:
- """Allow for changing attributes of the wrapped object.
-
- * If wrapped object isn't yet set, fall back to :meth:`~object.__setattr__` of this class.
- * If wrapped object doesn't already contain the attribute we want to set, also fallback to this class.
- * Otherwise, run ``__setattr__`` on it to update it.
- """
- try:
- wrapped = getattr(self, self._WRAPPED_ATTRIBUTE)
- except AttributeError:
- return super().__setattr__(name, value)
- else:
- if hasattr(wrapped, name):
- return setattr(wrapped, name, value)
-
- return super().__setattr__(name, value)
-
-
-class UnpropagatingMockMixin(Generic[T_Mock]):
- """Provides common functionality for our :class:`~unittest.mock.Mock` classes.
-
- By default, mock objects propagate themselves by returning a new instance of the same mock
- class, with same initialization attributes. This is done whenever we're accessing new
- attributes that mock class.
-
- This propagation makes sense for simple mocks without any additional restrictions, however when
- dealing with limited mocks to some ``spec_set``, it doesn't usually make sense to propagate
- those same ``spec_set`` restrictions, since we generally don't have attributes/methods of a
- class be of/return the same class.
-
- This mixin class stops this propagation, and instead returns instances of specified mock class,
- defined in :attr:`.child_mock_type` class variable, which is by default set to
- :class:`~unittest.mock.MagicMock`, as it can safely represent most objects.
-
- .. note:
- This propagation handling will only be done for the mock classes that inherited from this
- mixin class. That means if the :attr:`.child_mock_type` is one of the regular mock classes,
- and the mock is propagated, a regular mock class is returned as that new attribute. This
- regular class then won't have the same overrides, and will therefore propagate itself, like
- any other mock class would.
-
- If you wish to counteract this, you can set the :attr:`.child_mock_type` to a mock class
- that also inherits from this mixin class, perhaps to your class itself, overriding any
- propagation recursively.
- """
-
- child_mock_type: T_Mock = unittest.mock.MagicMock
-
- # Since this is a mixin class, we can access some attributes defined in mock classes safely.
- # Define the types of these variables here, for proper static type analysis.
- _mock_sealed: bool
- _extract_mock_name: Callable[[], str]
-
- def _get_child_mock(self, **kwargs) -> T_Mock:
- """Make :attr:`.child_mock_type`` instances instead of instances of the same class.
-
- By default, this method creates a new mock instance of the same original class, and passes
- over the same initialization arguments. This overrides that behavior to instead create an
- instance of :attr:`.child_mock_type` class.
- """
- # Mocks can be sealed, in which case we wouldn't want to allow propagation of any kind
- # and rather raise an AttributeError, informing that given attr isn't accessible
- if self._mock_sealed:
- mock_name = self._extract_mock_name()
- obj_name = (
- f"{mock_name}.{kwargs['name']}"
- if "name" in kwargs
- else f"{mock_name}()"
- )
- raise AttributeError(f"Can't access {obj_name}, mock is sealed.")
-
- # Propagate any other children as simple `unittest.mock.Mock` instances
- # rather than `self.__class__` instances
- return self.child_mock_type(**kwargs)
-
-
-class CustomMockMixin(UnpropagatingMockMixin):
- """Provides common functionality for our custom mock types.
-
- * Stops propagation of same ``spec_set`` restricted mock in child mocks
- (see :class:`.UnpropagatingMockMixin` for more info)
- * Allows using the ``spec_set`` attribute as class attribute
- """
-
- spec_set = None
-
- def __init__(self, **kwargs):
- if "spec_set" in kwargs:
- self.spec_set = kwargs.pop("spec_set")
- super().__init__(spec_set=self.spec_set, **kwargs) # type: ignore # Mixin class, this __init__ is valid
diff --git a/tests/test_encryption.py b/tests/test_encryption.py
deleted file mode 100644
index 92538ad..0000000
--- a/tests/test_encryption.py
+++ /dev/null
@@ -1,110 +0,0 @@
-# This is the buffer module from https://github.com/py-mine/mcproto v0.5.0,
-# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0
-
-from __future__ import annotations
-
-__author__ = "ItsDrike"
-__license__ = "LGPL-3.0-only"
-
-from typing import TYPE_CHECKING, cast
-
-from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP
-from cryptography.hazmat.primitives.hashes import SHA256
-from cryptography.hazmat.primitives.serialization import load_pem_private_key
-
-from azul.encryption import (
- decrypt_token_and_secret,
- deserialize_public_key,
- encrypt_token_and_secret,
- serialize_public_key,
-)
-
-if TYPE_CHECKING:
- from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
-
-_SERIALIZED_RSA_PRIVATE_KEY = b"""
------BEGIN PRIVATE KEY-----
-MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMtRUQmRHqPkdA2K
-F6fM2c8ibIPHYV5KVQXNEkVx7iEKS6JsfELhX1H8t/qQ3Ob4Pr4OFjgXx9n7GvfZ
-gekNoswG6lnQH/n7t2sYA 6D+WvSix1FF2J6wPmpKriHS59TDk4opjaV14S4K4XjW
-Gmm8DqCzgXkPGC2dunFb+1A8mdkrAgMBAAECgYAWj2dWkGu989OMzQ3i6LAic8dm
-t/Dt7YGRqzejzQiHUgUieLcxFKDnEAu6GejpGBKeNCHzB3B9l4deiRwJKCIwHqMN
-LKMKoayinA8mj/Y/ O/ELDofkEyeXOhFyM642sPpaxQJoNWc9QEsYbxpG2zeB3sPf
-l3eIhkYTKVdxB+o8AQJBAPiddMjU8fuHyjKT6VCL2ZQbwnrRe1AaLLE6VLwEZuZC
-wlbx5Lcszi77PkMRTvltQW39VN6MEjiYFSPtRJleA+sCQQDRW2e3BX6uiil2IZ08
-tPFMnltFJpa 8YvW50N6mySd8Zg1oQJpzP2fC0n0+K4j3EiA/Zli8jBt45cJ4dMGX
-km/BAkEAtkYy5j+BvolbDGP3Ti+KcRU9K/DD+QGHvNRoZYTQsIdHlpk4t7eo3zci
-+ecJwMOCkhKHE7cccNPHxBRkFBGiywJAJBt2pMsu0R2FDxm3C6xNXaCGL0P7hVwv
-8y9B51 QUGlFjiJJz0OKjm6c/8IQDqFEY/LZDIamsZ0qBItNIPEMGQQJALZV0GD5Y
-zmnkw1hek/JcfQBlVYo3gFmWBh6Hl1Lb7p3TKUViJCA1k2f0aGv7+d9aFS0fRq6u
-/sETkem8Jc1s3g==
------END PRIVATE KEY-----
-"""
-RSA_PRIVATE_KEY = cast(
- "RSAPrivateKey",
- load_pem_private_key(_SERIALIZED_RSA_PRIVATE_KEY, password=None),
-)
-RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key()
-SERIALIZED_RSA_PUBLIC_KEY = bytes.fromhex(
- "30819f300d06092a864886f70d010101050003818d0030818902818100cb515109911ea3e4740d8a17a7ccd9cf226c83c7615e4a5505cd124571ee210a4ba26c7c42e15f51fcb7fa90dce6f83ebe0e163817c7d9fb1af7d981e90da2cc06ea59d01ff9fbb76b1803a0fe5af4a2c75145d89eb03e6a4aae21d2e7d4c3938a298da575e12e0ae178d61a69bc0ea0b381790f182d9dba715bfb503c99d92b0203010001",
-)
-
-
-def test_encrypt_token_and_secret() -> None:
- """Test encryption returns properly encrypted (decryptable) values."""
- verification_token = bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8")
- shared_secret = bytes.fromhex(
- "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26",
- )
-
- encrypted_token, encrypted_secret = encrypt_token_and_secret(
- RSA_PUBLIC_KEY,
- verification_token,
- shared_secret,
- )
-
- assert (
- RSA_PRIVATE_KEY.decrypt(
- encrypted_token,
- OAEP(MGF1(SHA256()), SHA256(), None),
- )
- == verification_token
- )
- assert (
- RSA_PRIVATE_KEY.decrypt(
- encrypted_secret,
- OAEP(MGF1(SHA256()), SHA256(), None),
- )
- == shared_secret
- )
-
-
-def test_decrypt_token_and_secret() -> None:
- """Test decryption returns properly decrypted values."""
- encrypted_token = bytes.fromhex(
- "5541c0c0fc99d8908ed428b20c260795bec7b4041a4f98d26fbed383e8dba077eb53fb5cf905e722e2ceb341843e875508134817bcd3a909ac279e77ed94fd98c428bbe00db630a5ad3df310380d9274ed369cc6a011e7edd45cbe44ae8ad2575ef793b23057e4b15f1b6e3e195ff0921e46370773218517922fbb8b96092d88",
- )
- encrypted_secret = bytes.fromhex(
- "1a43782ca17f71e87e6ef98f9be66050ecf5d185da81445d26ceb5941f95d69d61b726d27b5ca62aed4cbe27b40fd4bd6b16b5be154a7b6a24ae31c705bc47d9397589b448fb72b14572ea2a9d843c6a3c674b7454cef97e2d65be36e0d0a8cc9f1093a19a8d52a5633a5317d19779bb46146dfaea7a690a7f080fb77d59c7f9",
- )
-
- assert decrypt_token_and_secret(
- RSA_PRIVATE_KEY,
- encrypted_token,
- encrypted_secret,
- ) == (
- bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8"),
- bytes.fromhex(
- "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26",
- ),
- )
-
-
-def test_serialize_public_key() -> None:
- """Test serialize_public_key."""
- assert serialize_public_key(RSA_PUBLIC_KEY) == SERIALIZED_RSA_PUBLIC_KEY
-
-
-def test_deserialize_public_key() -> None:
- """Test deserialize_public_key."""
- assert deserialize_public_key(SERIALIZED_RSA_PUBLIC_KEY) == RSA_PUBLIC_KEY
From ad70b0e274d9842ea23a74ec3b2cc781fc58392a Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 6 Feb 2026 21:01:54 -0600
Subject: [PATCH 59/67] Update project config and move computer players folder
---
.pre-commit-config.yaml | 14 +-
check.sh | 18 +-
ci.sh | 5 +-
pyproject.toml | 28 +-
.../azul_computer_players}/MiniMax_AI.py | 0
.../azul_computer_players}/machine_client.py | 0
.../azul_computer_players}/minimax.py | 0
uv.lock | 1279 ++++++++++-------
8 files changed, 783 insertions(+), 561 deletions(-)
rename {computer_players => src/azul_computer_players}/MiniMax_AI.py (100%)
rename {computer_players => src/azul_computer_players}/machine_client.py (100%)
rename {computer_players => src/azul_computer_players}/minimax.py (100%)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 671e5d8..1132490 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -19,14 +19,11 @@ repos:
- id: check-added-large-files
- id: sort-simple-yaml
files: .pre-commit-config.yaml
- - repo: https://github.com/psf/black-pre-commit-mirror
- rev: 26.1.0
- hooks:
- - id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.14.14
+ rev: v0.15.0
hooks:
- - id: ruff
+ - id: ruff-format
+ - id: ruff-check
types: [file]
types_or: [python, pyi, toml]
args: ["--show-fixes"]
@@ -41,10 +38,11 @@ repos:
additional_dependencies:
- tomli
- repo: https://github.com/adhtruong/mirrors-typos
- rev: v1.34.0
+ rev: v1.43.2
hooks:
- id: typos
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v1.11.0
+ rev: v1.22.0
hooks:
- id: zizmor
+ args: ["--fix", "--no-progress"]
diff --git a/check.sh b/check.sh
index c382508..e33b51c 100755
--- a/check.sh
+++ b/check.sh
@@ -4,7 +4,6 @@ set -ex
ON_GITHUB_CI=true
EXIT_STATUS=0
-PROJECT='azul'
# If not running on Github's CI, discard the summaries
if [ -z "${GITHUB_STEP_SUMMARY+x}" ]; then
@@ -13,17 +12,13 @@ if [ -z "${GITHUB_STEP_SUMMARY+x}" ]; then
fi
# Autoformatter *first*, to avoid double-reporting errors
-# (we'd like to run further autoformatters but *after* merging;
-# see https://forum.bors.tech/t/pre-test-and-pre-merge-hooks/322)
-# autoflake --recursive --in-place .
-# pyupgrade --py3-plus $(find . -name "*.py")
-echo "::group::Black"
-if ! black --check src/$PROJECT; then
- echo "* Black found issues" >> "$GITHUB_STEP_SUMMARY"
+echo "::group::Ruff format"
+if ! ruff format --check; then
+ echo "* Ruff formatting found issues" >> "$GITHUB_STEP_SUMMARY"
EXIT_STATUS=1
- black --diff src/$PROJECT
+ ruff format --diff
echo "::endgroup::"
- echo "::error:: Black found issues"
+ echo "::error:: Ruff formatting found issues"
else
echo "::endgroup::"
fi
@@ -98,8 +93,7 @@ Problems were found by static analysis (listed above).
To fix formatting and see remaining errors, run
uv sync --extra tools
- black src/$PROJECT
- ruff check src/$PROJECT
+ ruff check src
mypy
./check.sh
diff --git a/ci.sh b/ci.sh
index 02ea341..973c039 100755
--- a/ci.sh
+++ b/ci.sh
@@ -28,7 +28,7 @@ python -m uv --version
UV_VENV_SEED="pip"
python -m uv venv --seed --allow-existing
-# Determine platform and activate virtual environment accordingly
+# Determine the platform and activate the virtual environment accordingly
case "$OSTYPE" in
linux-gnu*|linux-musl*|darwin*)
source .venv/bin/activate
@@ -75,9 +75,6 @@ else
INSTALLDIR=$(python -c "import os, $PROJECT; print(os.path.dirname($PROJECT.__file__))")
cp ../pyproject.toml "$INSTALLDIR"
- # get mypy tests a nice cache
- MYPYPATH=".." mypy --config-file= --cache-dir=./.mypy_cache -c "import $PROJECT" >/dev/null 2>/dev/null || true
-
echo "::endgroup::"
echo "::group:: Run Tests"
if coverage run --rcfile=../pyproject.toml -m pytest -ra --junitxml=../test-results.xml ../tests --verbose --durations=10 $flags; then
diff --git a/pyproject.toml b/pyproject.toml
index 42327a1..08f0e4d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ authors = [
description = "Graphical Azul Game with AI support"
readme = {file = "README.md", content-type = "text/markdown"}
license = {file = "LICENSE"}
-requires-python = ">=3.10"
+requires-python = ">=3.11"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: End Users/Desktop",
@@ -37,12 +37,12 @@ keywords = [
]
dependencies = [
"exceptiongroup>=1.2.2; python_version < '3.11'",
- "libcomponent~=0.0.4",
+ "libcomponent~=0.0.5",
"mypy_extensions>=1.0.0",
- "numpy~=2.1.3",
+ "numpy~=2.4.2",
"orjson>=3.10,<4",
"pygame~=2.6.0",
- "trio~=0.30.0",
+ "trio~=0.32.0",
]
[tool.setuptools.dynamic]
@@ -61,13 +61,13 @@ tests = [
"pytest-cov>=6.0.0",
"pytest-trio>=0.8.0",
"coverage>=7.2.5",
- "uv>=0.5.21",
- "mypy>=1.14.1",
]
tools = [
- 'black>=24.10.0; implementation_name == "cpython"',
+ "uv>=0.10.0",
+ "mypy>=1.14.1",
"ruff>=0.9.2",
"codespell>=2.3.0",
+ "pre-commit>=4.2.0",
]
[tool.setuptools.package-data]
@@ -77,8 +77,16 @@ azul = ["py.typed", "data/*", "lang/*", "fonts/*"]
package = true
[tool.mypy]
-files = ["src/azul/", "computer_players"]
-enable_error_code = ["truthy-bool", "mutable-override"]
+files = [
+ "src/azul/",
+ "src/azul_computer_players",
+ "tests",
+]
+enable_error_code = [
+ "truthy-bool",
+ "mutable-override",
+ "exhaustive-match",
+]
show_column_numbers = true
show_error_codes = true
show_traceback = true
@@ -158,7 +166,7 @@ extend-ignore = [
]
[tool.pytest.ini_options]
-addopts = "--cov-report=xml --cov-report=term-missing --cov=azul"
+addopts = "--cov-report=term-missing --cov=azul"
testpaths = [
"tests",
]
diff --git a/computer_players/MiniMax_AI.py b/src/azul_computer_players/MiniMax_AI.py
similarity index 100%
rename from computer_players/MiniMax_AI.py
rename to src/azul_computer_players/MiniMax_AI.py
diff --git a/computer_players/machine_client.py b/src/azul_computer_players/machine_client.py
similarity index 100%
rename from computer_players/machine_client.py
rename to src/azul_computer_players/machine_client.py
diff --git a/computer_players/minimax.py b/src/azul_computer_players/minimax.py
similarity index 100%
rename from computer_players/minimax.py
rename to src/azul_computer_players/minimax.py
diff --git a/uv.lock b/uv.lock
index caf9687..2a61f1c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,21 +1,20 @@
version = 1
-revision = 1
-requires-python = ">=3.10"
+revision = 3
+requires-python = ">=3.11"
[[package]]
name = "attrs"
-version = "25.3.0"
+version = "25.4.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
[[package]]
name = "azul"
source = { editable = "." }
dependencies = [
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "libcomponent" },
{ name = "mypy-extensions" },
{ name = "numpy" },
@@ -27,218 +26,221 @@ dependencies = [
[package.optional-dependencies]
tests = [
{ name = "coverage" },
- { name = "mypy" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-trio" },
- { name = "uv" },
]
tools = [
- { name = "black", marker = "implementation_name == 'cpython'" },
{ name = "codespell" },
+ { name = "mypy" },
+ { name = "pre-commit" },
{ name = "ruff" },
+ { name = "uv" },
]
[package.metadata]
requires-dist = [
- { name = "black", marker = "implementation_name == 'cpython' and extra == 'tools'", specifier = ">=24.10.0" },
{ name = "codespell", marker = "extra == 'tools'", specifier = ">=2.3.0" },
{ name = "coverage", marker = "extra == 'tests'", specifier = ">=7.2.5" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'", specifier = ">=1.2.2" },
- { name = "libcomponent", specifier = "~=0.0.4" },
- { name = "mypy", marker = "extra == 'tests'", specifier = ">=1.14.1" },
+ { name = "libcomponent", specifier = "~=0.0.5" },
+ { name = "mypy", marker = "extra == 'tools'", specifier = ">=1.14.1" },
{ name = "mypy-extensions", specifier = ">=1.0.0" },
- { name = "numpy", specifier = "~=2.1.3" },
+ { name = "numpy", specifier = "~=2.4.2" },
{ name = "orjson", specifier = ">=3.10,<4" },
+ { name = "pre-commit", marker = "extra == 'tools'", specifier = ">=4.2.0" },
{ name = "pygame", specifier = "~=2.6.0" },
{ name = "pytest", marker = "extra == 'tests'", specifier = ">=5.0" },
{ name = "pytest-cov", marker = "extra == 'tests'", specifier = ">=6.0.0" },
{ name = "pytest-trio", marker = "extra == 'tests'", specifier = ">=0.8.0" },
{ name = "ruff", marker = "extra == 'tools'", specifier = ">=0.9.2" },
- { name = "trio", specifier = "~=0.30.0" },
- { name = "uv", marker = "extra == 'tests'", specifier = ">=0.5.21" },
+ { name = "trio", specifier = "~=0.32.0" },
+ { name = "uv", marker = "extra == 'tools'", specifier = ">=0.10.0" },
]
provides-extras = ["tests", "tools"]
-[[package]]
-name = "black"
-version = "25.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "mypy-extensions" },
- { name = "packaging" },
- { name = "pathspec" },
- { name = "platformdirs" },
- { name = "tomli", marker = "python_full_version < '3.11'" },
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 },
- { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 },
- { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 },
- { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 },
- { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 },
- { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 },
- { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 },
- { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 },
- { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 },
- { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 },
- { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 },
- { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 },
- { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
- { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
- { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
- { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
- { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
-]
-
[[package]]
name = "cffi"
-version = "1.17.1"
+version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pycparser" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 },
- { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 },
- { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 },
- { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 },
- { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 },
- { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 },
- { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 },
- { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 },
- { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 },
- { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 },
- { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 },
- { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 },
- { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 },
- { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 },
- { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
- { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
- { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
- { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
- { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
- { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
- { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
- { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
- { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
- { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 },
- { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
- { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
- { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
- { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
- { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
- { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
- { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
- { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
- { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
- { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
- { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
- { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
- { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
- { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
- { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
- { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
- { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
- { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
- { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
- { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
- { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
- { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
-]
-
-[[package]]
-name = "click"
-version = "8.1.8"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
]
[[package]]
name = "codespell"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740 }
+sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501 },
+ { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
-version = "7.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379 },
- { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814 },
- { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937 },
- { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849 },
- { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986 },
- { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896 },
- { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613 },
- { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909 },
- { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948 },
- { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844 },
- { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 },
- { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 },
- { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 },
- { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 },
- { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 },
- { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 },
- { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 },
- { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 },
- { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 },
- { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 },
- { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 },
- { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 },
- { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 },
- { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 },
- { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 },
- { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 },
- { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 },
- { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 },
- { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 },
- { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 },
- { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 },
- { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 },
- { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 },
- { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 },
- { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 },
- { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 },
- { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 },
- { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 },
- { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 },
- { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 },
- { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 },
- { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 },
- { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 },
- { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 },
- { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 },
- { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 },
- { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 },
- { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 },
- { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 },
- { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 },
- { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 },
- { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 },
+version = "7.13.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" },
+ { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" },
+ { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" },
+ { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" },
+ { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" },
+ { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" },
+ { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" },
+ { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" },
+ { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" },
+ { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" },
+ { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" },
+ { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" },
+ { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" },
+ { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" },
+ { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" },
+ { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" },
+ { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" },
+ { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" },
+ { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" },
+ { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" },
+ { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" },
+ { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" },
+ { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" },
+ { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" },
+ { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" },
+ { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" },
+ { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" },
+ { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" },
+ { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" },
+ { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" },
]
[package.optional-dependencies]
@@ -248,81 +250,111 @@ toml = [
[[package]]
name = "cryptography"
-version = "44.0.3"
+version = "46.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281 },
- { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305 },
- { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040 },
- { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411 },
- { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263 },
- { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198 },
- { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502 },
- { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173 },
- { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713 },
- { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064 },
- { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887 },
- { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737 },
- { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501 },
- { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307 },
- { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876 },
- { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127 },
- { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164 },
- { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081 },
- { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716 },
- { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398 },
- { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900 },
- { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067 },
- { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467 },
- { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375 },
- { url = "https://files.pythonhosted.org/packages/7f/10/abcf7418536df1eaba70e2cfc5c8a0ab07aa7aa02a5cbc6a78b9d8b4f121/cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d", size = 3393192 },
- { url = "https://files.pythonhosted.org/packages/06/59/ecb3ef380f5891978f92a7f9120e2852b1df6f0a849c277b8ea45b865db2/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8", size = 3898419 },
- { url = "https://files.pythonhosted.org/packages/bb/d0/35e2313dbb38cf793aa242182ad5bc5ef5c8fd4e5dbdc380b936c7d51169/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4", size = 4117892 },
- { url = "https://files.pythonhosted.org/packages/dc/c8/31fb6e33b56c2c2100d76de3fd820afaa9d4d0b6aea1ccaf9aaf35dc7ce3/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff", size = 3900855 },
- { url = "https://files.pythonhosted.org/packages/43/2a/08cc2ec19e77f2a3cfa2337b429676406d4bb78ddd130a05c458e7b91d73/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06", size = 4117619 },
- { url = "https://files.pythonhosted.org/packages/02/68/fc3d3f84022a75f2ac4b1a1c0e5d6a0c2ea259e14cd4aae3e0e68e56483c/cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9", size = 3136570 },
- { url = "https://files.pythonhosted.org/packages/8d/4b/c11ad0b6c061902de5223892d680e89c06c7c4d606305eb8de56c5427ae6/cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375", size = 3390230 },
- { url = "https://files.pythonhosted.org/packages/58/11/0a6bf45d53b9b2290ea3cec30e78b78e6ca29dc101e2e296872a0ffe1335/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647", size = 3895216 },
- { url = "https://files.pythonhosted.org/packages/0a/27/b28cdeb7270e957f0077a2c2bfad1b38f72f1f6d699679f97b816ca33642/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259", size = 4115044 },
- { url = "https://files.pythonhosted.org/packages/35/b0/ec4082d3793f03cb248881fecefc26015813199b88f33e3e990a43f79835/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff", size = 3898034 },
- { url = "https://files.pythonhosted.org/packages/0b/7f/adf62e0b8e8d04d50c9a91282a57628c00c54d4ae75e2b02a223bd1f2613/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5", size = 4114449 },
- { url = "https://files.pythonhosted.org/packages/87/62/d69eb4a8ee231f4bf733a92caf9da13f1c81a44e874b1d4080c25ecbb723/cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c", size = 3134369 },
-]
-
-[[package]]
-name = "exceptiongroup"
-version = "1.2.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
+sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" },
+ { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" },
+ { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" },
+ { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" },
+ { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" },
+ { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" },
+ { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" },
+ { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" },
+ { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" },
+ { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" },
+ { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" },
+ { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" },
+ { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" },
+ { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" },
+ { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" },
+ { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
+ { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" },
+ { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" },
+ { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.20.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.16"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" },
]
[[package]]
name = "idna"
-version = "3.10"
+version = "3.11"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "iniconfig"
-version = "2.1.0"
+version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "libcomponent"
-version = "0.0.4"
+version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
@@ -330,184 +362,276 @@ dependencies = [
{ name = "trio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9a/0a/5f3d65ed7bb368baa69c067a1ef478a994b7326c6747a45fc834e8443890/libcomponent-0.0.4.tar.gz", hash = "sha256:c1daa699d748be0e0132c4062e0527a89a6a8dff32c291efa9880c0715d49c1b", size = 76346 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c4/ab/c804e77e769a14bace0ff7d536a2c2b9fb08289a0cab7f76840c92a270b6/libcomponent-0.0.4-py3-none-any.whl", hash = "sha256:c26edf821564da1400ede72d1414bc754cd693199d436e3cde7b46b9950d7587", size = 59380 },
+sdist = { url = "https://files.pythonhosted.org/packages/ec/4a/e041aa4a2019af6d10aefc0f861041579980b4629aae4bc2bc72d7ea89af/libcomponent-0.0.5.tar.gz", hash = "sha256:679602a93e6b3fd58811d6e7866f6ab096611ecf615ef3a573bec6455c93e809", size = 41202, upload-time = "2025-09-20T05:20:00.252Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/22/f3c6338ea0facf7740a2f67bf331b083a86532c163f80a4bf3fbbf6fb1e8/libcomponent-0.0.5-py3-none-any.whl", hash = "sha256:bd3f05e3085619ddf14f1727eff81d1160fdd2ee63bf5db4372da5c22d0c8687", size = 37244, upload-time = "2025-09-20T05:19:58.718Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" },
+ { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" },
+ { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" },
+ { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" },
+ { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" },
+ { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" },
+ { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" },
+ { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" },
+ { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" },
+ { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" },
+ { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" },
+ { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" },
+ { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
]
[[package]]
name = "mypy"
-version = "1.15.0"
+version = "1.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
- { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "pathspec" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 },
- { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 },
- { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 },
- { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 },
- { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 },
- { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 },
- { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 },
- { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 },
- { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 },
- { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 },
- { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 },
- { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 },
- { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 },
- { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 },
- { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 },
- { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 },
- { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 },
- { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 },
- { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
- { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
- { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
- { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
- { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
- { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
- { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
+sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
+ { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
+ { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
+ { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
+ { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
+ { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
+ { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
+ { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
+ { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
+ { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
]
[[package]]
name = "numpy"
-version = "2.1.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f1/80/d572a4737626372915bca41c3afbfec9d173561a39a0a61bacbbfd1dafd4/numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", size = 21152472 },
- { url = "https://files.pythonhosted.org/packages/6f/bb/7bfba10c791ae3bb6716da77ad85a82d5fac07fc96fb0023ef0571df9d20/numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", size = 13747967 },
- { url = "https://files.pythonhosted.org/packages/da/d6/2df7bde35f0478455f0be5934877b3e5a505f587b00230f54a519a6b55a5/numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", size = 5354921 },
- { url = "https://files.pythonhosted.org/packages/d1/bb/75b945874f931494891eac6ca06a1764d0e8208791f3addadb2963b83527/numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", size = 6888603 },
- { url = "https://files.pythonhosted.org/packages/68/a7/fde73636f6498dbfa6d82fc336164635fe592f1ad0d13285fcb6267fdc1c/numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", size = 13889862 },
- { url = "https://files.pythonhosted.org/packages/05/db/5d9c91b2e1e2e72be1369278f696356d44975befcae830daf2e667dcb54f/numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", size = 16328151 },
- { url = "https://files.pythonhosted.org/packages/3e/6a/7eb732109b53ae64a29e25d7e68eb9d6611037f6354875497008a49e74d3/numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", size = 16704107 },
- { url = "https://files.pythonhosted.org/packages/88/cc/278113b66a1141053cbda6f80e4200c6da06b3079c2d27bda1fde41f2c1f/numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4", size = 14385789 },
- { url = "https://files.pythonhosted.org/packages/f5/69/eb20f5e1bfa07449bc67574d2f0f7c1e6b335fb41672e43861a7727d85f2/numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", size = 6536706 },
- { url = "https://files.pythonhosted.org/packages/8e/8b/1c131ab5a94c1086c289c6e1da1d843de9dbd95fe5f5ee6e61904c9518e2/numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", size = 12864165 },
- { url = "https://files.pythonhosted.org/packages/ad/81/c8167192eba5247593cd9d305ac236847c2912ff39e11402e72ae28a4985/numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", size = 21156252 },
- { url = "https://files.pythonhosted.org/packages/da/74/5a60003fc3d8a718d830b08b654d0eea2d2db0806bab8f3c2aca7e18e010/numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", size = 13784119 },
- { url = "https://files.pythonhosted.org/packages/47/7c/864cb966b96fce5e63fcf25e1e4d957fe5725a635e5f11fe03f39dd9d6b5/numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", size = 5352978 },
- { url = "https://files.pythonhosted.org/packages/09/ac/61d07930a4993dd9691a6432de16d93bbe6aa4b1c12a5e573d468eefc1ca/numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", size = 6892570 },
- { url = "https://files.pythonhosted.org/packages/27/2f/21b94664f23af2bb52030653697c685022119e0dc93d6097c3cb45bce5f9/numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", size = 13896715 },
- { url = "https://files.pythonhosted.org/packages/7a/f0/80811e836484262b236c684a75dfc4ba0424bc670e765afaa911468d9f39/numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", size = 16339644 },
- { url = "https://files.pythonhosted.org/packages/fa/81/ce213159a1ed8eb7d88a2a6ef4fbdb9e4ffd0c76b866c350eb4e3c37e640/numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", size = 16712217 },
- { url = "https://files.pythonhosted.org/packages/7d/84/4de0b87d5a72f45556b2a8ee9fc8801e8518ec867fc68260c1f5dcb3903f/numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", size = 14399053 },
- { url = "https://files.pythonhosted.org/packages/7e/1c/e5fabb9ad849f9d798b44458fd12a318d27592d4bc1448e269dec070ff04/numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", size = 6534741 },
- { url = "https://files.pythonhosted.org/packages/1e/48/a9a4b538e28f854bfb62e1dea3c8fea12e90216a276c7777ae5345ff29a7/numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", size = 12869487 },
- { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658 },
- { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258 },
- { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249 },
- { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704 },
- { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089 },
- { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185 },
- { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751 },
- { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705 },
- { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077 },
- { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858 },
- { url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263 },
- { url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771 },
- { url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805 },
- { url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380 },
- { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451 },
- { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822 },
- { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822 },
- { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598 },
- { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021 },
- { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405 },
- { url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062 },
- { url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839 },
- { url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031 },
- { url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977 },
- { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951 },
- { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655 },
- { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902 },
- { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180 },
- { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907 },
- { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 },
- { url = "https://files.pythonhosted.org/packages/00/e7/8d8bb791b62586cc432ecbb70632b4f23b7b7c88df41878de7528264f6d7/numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", size = 20983893 },
- { url = "https://files.pythonhosted.org/packages/5e/f3/cb8118a044b5007586245a650360c9f5915b2f4232dd7658bb7a63dd1d02/numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", size = 6752501 },
- { url = "https://files.pythonhosted.org/packages/53/f5/365b46439b518d2ec6ebb880cc0edf90f225145dfd4db7958334f7164530/numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", size = 16142601 },
- { url = "https://files.pythonhosted.org/packages/03/c2/d1fee6ba999aa7cd41ca6856937f2baaf604c3eec1565eae63451ec31e5e/numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", size = 12771397 },
+version = "2.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" },
+ { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" },
+ { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" },
+ { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" },
+ { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" },
+ { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" },
+ { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" },
+ { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" },
+ { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" },
+ { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" },
+ { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" },
+ { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" },
+ { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" },
+ { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" },
+ { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" },
+ { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" },
+ { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" },
+ { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" },
+ { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" },
+ { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" },
+ { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" },
+ { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" },
+ { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" },
+ { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" },
+ { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" },
+ { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" },
+ { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" },
+ { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" },
+ { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" },
+ { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" },
+ { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" },
+ { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" },
]
[[package]]
name = "orjson"
-version = "3.10.18"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/27/16/2ceb9fb7bc2b11b1e4a3ea27794256e93dee2309ebe297fd131a778cd150/orjson-3.10.18-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a45e5d68066b408e4bc383b6e4ef05e717c65219a9e1390abc6155a520cac402", size = 248927 },
- { url = "https://files.pythonhosted.org/packages/3d/e1/d3c0a2bba5b9906badd121da449295062b289236c39c3a7801f92c4682b0/orjson-3.10.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be3b9b143e8b9db05368b13b04c84d37544ec85bb97237b3a923f076265ec89c", size = 136995 },
- { url = "https://files.pythonhosted.org/packages/d7/51/698dd65e94f153ee5ecb2586c89702c9e9d12f165a63e74eb9ea1299f4e1/orjson-3.10.18-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9b0aa09745e2c9b3bf779b096fa71d1cc2d801a604ef6dd79c8b1bfef52b2f92", size = 132893 },
- { url = "https://files.pythonhosted.org/packages/b3/e5/155ce5a2c43a85e790fcf8b985400138ce5369f24ee6770378ee6b691036/orjson-3.10.18-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53a245c104d2792e65c8d225158f2b8262749ffe64bc7755b00024757d957a13", size = 137017 },
- { url = "https://files.pythonhosted.org/packages/46/bb/6141ec3beac3125c0b07375aee01b5124989907d61c72c7636136e4bd03e/orjson-3.10.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9495ab2611b7f8a0a8a505bcb0f0cbdb5469caafe17b0e404c3c746f9900469", size = 138290 },
- { url = "https://files.pythonhosted.org/packages/77/36/6961eca0b66b7809d33c4ca58c6bd4c23a1b914fb23aba2fa2883f791434/orjson-3.10.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73be1cbcebadeabdbc468f82b087df435843c809cd079a565fb16f0f3b23238f", size = 142828 },
- { url = "https://files.pythonhosted.org/packages/8b/2f/0c646d5fd689d3be94f4d83fa9435a6c4322c9b8533edbb3cd4bc8c5f69a/orjson-3.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8936ee2679e38903df158037a2f1c108129dee218975122e37847fb1d4ac68", size = 132806 },
- { url = "https://files.pythonhosted.org/packages/ea/af/65907b40c74ef4c3674ef2bcfa311c695eb934710459841b3c2da212215c/orjson-3.10.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7115fcbc8525c74e4c2b608129bef740198e9a120ae46184dac7683191042056", size = 135005 },
- { url = "https://files.pythonhosted.org/packages/c7/d1/68bd20ac6a32cd1f1b10d23e7cc58ee1e730e80624e3031d77067d7150fc/orjson-3.10.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:771474ad34c66bc4d1c01f645f150048030694ea5b2709b87d3bda273ffe505d", size = 413418 },
- { url = "https://files.pythonhosted.org/packages/31/31/c701ec0bcc3e80e5cb6e319c628ef7b768aaa24b0f3b4c599df2eaacfa24/orjson-3.10.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c14047dbbea52886dd87169f21939af5d55143dad22d10db6a7514f058156a8", size = 153288 },
- { url = "https://files.pythonhosted.org/packages/d9/31/5e1aa99a10893a43cfc58009f9da840990cc8a9ebb75aa452210ba18587e/orjson-3.10.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641481b73baec8db14fdf58f8967e52dc8bda1f2aba3aa5f5c1b07ed6df50b7f", size = 137181 },
- { url = "https://files.pythonhosted.org/packages/bf/8c/daba0ac1b8690011d9242a0f37235f7d17df6d0ad941021048523b76674e/orjson-3.10.18-cp310-cp310-win32.whl", hash = "sha256:607eb3ae0909d47280c1fc657c4284c34b785bae371d007595633f4b1a2bbe06", size = 142694 },
- { url = "https://files.pythonhosted.org/packages/16/62/8b687724143286b63e1d0fab3ad4214d54566d80b0ba9d67c26aaf28a2f8/orjson-3.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:8770432524ce0eca50b7efc2a9a5f486ee0113a5fbb4231526d414e6254eba92", size = 134600 },
- { url = "https://files.pythonhosted.org/packages/97/c7/c54a948ce9a4278794f669a353551ce7db4ffb656c69a6e1f2264d563e50/orjson-3.10.18-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e0a183ac3b8e40471e8d843105da6fbe7c070faab023be3b08188ee3f85719b8", size = 248929 },
- { url = "https://files.pythonhosted.org/packages/9e/60/a9c674ef1dd8ab22b5b10f9300e7e70444d4e3cda4b8258d6c2488c32143/orjson-3.10.18-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5ef7c164d9174362f85238d0cd4afdeeb89d9e523e4651add6a5d458d6f7d42d", size = 133364 },
- { url = "https://files.pythonhosted.org/packages/c1/4e/f7d1bdd983082216e414e6d7ef897b0c2957f99c545826c06f371d52337e/orjson-3.10.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd14c5d99cdc7bf93f22b12ec3b294931518aa019e2a147e8aa2f31fd3240f7", size = 136995 },
- { url = "https://files.pythonhosted.org/packages/17/89/46b9181ba0ea251c9243b0c8ce29ff7c9796fa943806a9c8b02592fce8ea/orjson-3.10.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b672502323b6cd133c4af6b79e3bea36bad2d16bca6c1f645903fce83909a7a", size = 132894 },
- { url = "https://files.pythonhosted.org/packages/ca/dd/7bce6fcc5b8c21aef59ba3c67f2166f0a1a9b0317dcca4a9d5bd7934ecfd/orjson-3.10.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51f8c63be6e070ec894c629186b1c0fe798662b8687f3d9fdfa5e401c6bd7679", size = 137016 },
- { url = "https://files.pythonhosted.org/packages/1c/4a/b8aea1c83af805dcd31c1f03c95aabb3e19a016b2a4645dd822c5686e94d/orjson-3.10.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9478ade5313d724e0495d167083c6f3be0dd2f1c9c8a38db9a9e912cdaf947", size = 138290 },
- { url = "https://files.pythonhosted.org/packages/36/d6/7eb05c85d987b688707f45dcf83c91abc2251e0dd9fb4f7be96514f838b1/orjson-3.10.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:187aefa562300a9d382b4b4eb9694806e5848b0cedf52037bb5c228c61bb66d4", size = 142829 },
- { url = "https://files.pythonhosted.org/packages/d2/78/ddd3ee7873f2b5f90f016bc04062713d567435c53ecc8783aab3a4d34915/orjson-3.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da552683bc9da222379c7a01779bddd0ad39dd699dd6300abaf43eadee38334", size = 132805 },
- { url = "https://files.pythonhosted.org/packages/8c/09/c8e047f73d2c5d21ead9c180203e111cddeffc0848d5f0f974e346e21c8e/orjson-3.10.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e450885f7b47a0231979d9c49b567ed1c4e9f69240804621be87c40bc9d3cf17", size = 135008 },
- { url = "https://files.pythonhosted.org/packages/0c/4b/dccbf5055ef8fb6eda542ab271955fc1f9bf0b941a058490293f8811122b/orjson-3.10.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5e3c9cc2ba324187cd06287ca24f65528f16dfc80add48dc99fa6c836bb3137e", size = 413419 },
- { url = "https://files.pythonhosted.org/packages/8a/f3/1eac0c5e2d6d6790bd2025ebfbefcbd37f0d097103d76f9b3f9302af5a17/orjson-3.10.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50ce016233ac4bfd843ac5471e232b865271d7d9d44cf9d33773bcd883ce442b", size = 153292 },
- { url = "https://files.pythonhosted.org/packages/1f/b4/ef0abf64c8f1fabf98791819ab502c2c8c1dc48b786646533a93637d8999/orjson-3.10.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3ceff74a8f7ffde0b2785ca749fc4e80e4315c0fd887561144059fb1c138aa7", size = 137182 },
- { url = "https://files.pythonhosted.org/packages/a9/a3/6ea878e7b4a0dc5c888d0370d7752dcb23f402747d10e2257478d69b5e63/orjson-3.10.18-cp311-cp311-win32.whl", hash = "sha256:fdba703c722bd868c04702cac4cb8c6b8ff137af2623bc0ddb3b3e6a2c8996c1", size = 142695 },
- { url = "https://files.pythonhosted.org/packages/79/2a/4048700a3233d562f0e90d5572a849baa18ae4e5ce4c3ba6247e4ece57b0/orjson-3.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:c28082933c71ff4bc6ccc82a454a2bffcef6e1d7379756ca567c772e4fb3278a", size = 134603 },
- { url = "https://files.pythonhosted.org/packages/03/45/10d934535a4993d27e1c84f1810e79ccf8b1b7418cef12151a22fe9bb1e1/orjson-3.10.18-cp311-cp311-win_arm64.whl", hash = "sha256:a6c7c391beaedd3fa63206e5c2b7b554196f14debf1ec9deb54b5d279b1b46f5", size = 131400 },
- { url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184 },
- { url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279 },
- { url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799 },
- { url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791 },
- { url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059 },
- { url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359 },
- { url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853 },
- { url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131 },
- { url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834 },
- { url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368 },
- { url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359 },
- { url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466 },
- { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683 },
- { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754 },
- { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218 },
- { url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087 },
- { url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273 },
- { url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779 },
- { url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811 },
- { url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018 },
- { url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368 },
- { url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840 },
- { url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135 },
- { url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810 },
- { url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491 },
- { url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277 },
- { url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367 },
- { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687 },
- { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794 },
- { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186 },
+version = "3.11.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" },
+ { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" },
+ { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" },
+ { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" },
+ { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" },
+ { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" },
+ { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" },
+ { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" },
+ { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" },
+ { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" },
+ { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" },
+ { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" },
+ { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" },
+ { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" },
+ { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" },
+ { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" },
+ { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" },
+ { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" },
+ { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" },
+ { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" },
+ { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" },
+ { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" },
+ { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" },
+ { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" },
+ { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" },
+ { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" },
+ { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" },
]
[[package]]
@@ -517,120 +641,138 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 }
+sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 },
+ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
]
[[package]]
name = "packaging"
-version = "25.0"
+version = "26.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pathspec"
-version = "0.12.1"
+version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
+sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
+ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
]
[[package]]
name = "platformdirs"
-version = "4.3.7"
+version = "4.5.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 },
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
]
[[package]]
name = "pluggy"
-version = "1.5.0"
+version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
]
[[package]]
name = "pycparser"
-version = "2.22"
+version = "3.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pygame"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", hash = "sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f", size = 14808125 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/0b/334c7c50a2979e15f2a027a41d1ca78ee730d5b1c7f7f4b26d7cb899839d/pygame-2.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9beeb647e555afb5657111fa83acb74b99ad88761108eaea66472e8b8547b55b", size = 13109297 },
- { url = "https://files.pythonhosted.org/packages/dc/48/f8b1069788d1bd42e63a960d74d3355242480b750173a42b2749687578ca/pygame-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10e3d2a55f001f6c0a6eb44aa79ea7607091c9352b946692acedb2ac1482f1c9", size = 12375837 },
- { url = "https://files.pythonhosted.org/packages/bc/33/a1310386b8913ce1bdb90c33fa536970e299ad57eb35785f1d71ea1e2ad3/pygame-2.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816e85000c5d8b02a42b9834f761a5925ef3377d2924e3a7c4c143d2990ce5b8", size = 13607860 },
- { url = "https://files.pythonhosted.org/packages/88/0f/4e37b115056e43714e7550054dd3cd7f4d552da54d7fc58a2fb1407acda5/pygame-2.6.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a78fd030d98faab4a8e27878536fdff7518d3e062a72761c552f624ebba5a5f", size = 14304696 },
- { url = "https://files.pythonhosted.org/packages/11/b3/de6ed93ae483cf3bac8f950a955e83f7ffe59651fd804d100fff65d66d6c/pygame-2.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da3ad64d685f84a34ebe5daacb39fff14f1251acb34c098d760d63fee768f50c", size = 13977684 },
- { url = "https://files.pythonhosted.org/packages/d3/05/d86440aa879708c41844bafc6b3eb42c6d8cf54082482499b53139133e2a/pygame-2.6.1-cp310-cp310-win32.whl", hash = "sha256:9dd5c054d4bd875a8caf978b82672f02bec332f52a833a76899220c460bb4b58", size = 10251775 },
- { url = "https://files.pythonhosted.org/packages/38/88/8de61324775cf2c844a51d8db14a8a6d2a9092312f27678f6eaa3a460376/pygame-2.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:00827aba089355925902d533f9c41e79a799641f03746c50a374dc5c3362e43d", size = 10618801 },
- { url = "https://files.pythonhosted.org/packages/c4/ca/8f367cb9fe734c4f6f6400e045593beea2635cd736158f9fabf58ee14e3c/pygame-2.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:20349195326a5e82a16e351ed93465a7845a7e2a9af55b7bc1b2110ea3e344e1", size = 13113753 },
- { url = "https://files.pythonhosted.org/packages/83/47/6edf2f890139616b3219be9cfcc8f0cb8f42eb15efd59597927e390538cb/pygame-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3935459109da4bb0b3901da9904f0a3e52028a3332a355d298b1673a334cf21", size = 12378146 },
- { url = "https://files.pythonhosted.org/packages/00/9e/0d8aa8cf93db2d2ee38ebaf1c7b61d0df36ded27eb726221719c150c673d/pygame-2.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31dbdb5d0217f32764797d21c2752e258e5fb7e895326538d82b5f75a0cd856", size = 13611760 },
- { url = "https://files.pythonhosted.org/packages/d7/9e/d06adaa5cc65876bcd7a24f59f67e07f7e4194e6298130024ed3fb22c456/pygame-2.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:173badf82fa198e6888017bea40f511cb28e69ecdd5a72b214e81e4dcd66c3b1", size = 14298054 },
- { url = "https://files.pythonhosted.org/packages/7a/a1/9ae2852ebd3a7cc7d9ae7ff7919ab983e4a5c1b7a14e840732f23b2b48f6/pygame-2.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce8cc108b92de9b149b344ad2e25eedbe773af0dc41dfb24d1f07f679b558c60", size = 13977107 },
- { url = "https://files.pythonhosted.org/packages/31/df/6788fd2e9a864d0496a77670e44a7c012184b7a5382866ab0e60c55c0f28/pygame-2.6.1-cp311-cp311-win32.whl", hash = "sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c", size = 10250863 },
- { url = "https://files.pythonhosted.org/packages/d2/55/ca3eb851aeef4f6f2e98a360c201f0d00bd1ba2eb98e2c7850d80aabc526/pygame-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299", size = 10622016 },
- { url = "https://files.pythonhosted.org/packages/92/16/2c602c332f45ff9526d61f6bd764db5096ff9035433e2172e2d2cadae8db/pygame-2.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ee7f2771f588c966fa2fa8b829be26698c9b4836f82ede5e4edc1a68594942e", size = 13118279 },
- { url = "https://files.pythonhosted.org/packages/cd/53/77ccbc384b251c6e34bfd2e734c638233922449a7844e3c7a11ef91cee39/pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8040ea2ab18c6b255af706ec01355c8a6b08dc48d77fd4ee783f8fc46a843bf", size = 12384524 },
- { url = "https://files.pythonhosted.org/packages/06/be/3ed337583f010696c3b3435e89a74fb29d0c74d0931e8f33c0a4246307a9/pygame-2.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47a6938de93fa610accd4969e638c2aebcb29b2fca518a84c3a39d91ab47116", size = 13587123 },
- { url = "https://files.pythonhosted.org/packages/fd/ca/b015586a450db59313535662991b34d24c1f0c0dc149cc5f496573900f4e/pygame-2.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33006f784e1c7d7e466fcb61d5489da59cc5f7eb098712f792a225df1d4e229d", size = 14275532 },
- { url = "https://files.pythonhosted.org/packages/b9/f2/d31e6ad42d657af07be2ffd779190353f759a07b51232b9e1d724f2cda46/pygame-2.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1206125f14cae22c44565c9d333607f1d9f59487b1f1432945dfc809aeaa3e88", size = 13952653 },
- { url = "https://files.pythonhosted.org/packages/f3/42/8ea2a6979e6fa971702fece1747e862e2256d4a8558fe0da6364dd946c53/pygame-2.6.1-cp312-cp312-win32.whl", hash = "sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e", size = 10252421 },
- { url = "https://files.pythonhosted.org/packages/5f/90/7d766d54bb95939725e9a9361f9c06b0cfbe3fe100aa35400f0a461a278a/pygame-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65", size = 10624591 },
- { url = "https://files.pythonhosted.org/packages/e1/91/718acf3e2a9d08a6ddcc96bd02a6f63c99ee7ba14afeaff2a51c987df0b9/pygame-2.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2", size = 13090765 },
- { url = "https://files.pythonhosted.org/packages/0e/c6/9cb315de851a7682d9c7568a41ea042ee98d668cb8deadc1dafcab6116f0/pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171", size = 12381704 },
- { url = "https://files.pythonhosted.org/packages/9f/8f/617a1196e31ae3b46be6949fbaa95b8c93ce15e0544266198c2266cc1b4d/pygame-2.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b", size = 13581091 },
- { url = "https://files.pythonhosted.org/packages/3b/87/2851a564e40a2dad353f1c6e143465d445dab18a95281f9ea458b94f3608/pygame-2.6.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b", size = 14273844 },
- { url = "https://files.pythonhosted.org/packages/85/b5/aa23aa2e70bcba42c989c02e7228273c30f3b44b9b264abb93eaeff43ad7/pygame-2.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c", size = 13951197 },
- { url = "https://files.pythonhosted.org/packages/a6/06/29e939b34d3f1354738c7d201c51c250ad7abefefaf6f8332d962ff67c4b/pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e", size = 10249309 },
- { url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084 },
+sdist = { url = "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", hash = "sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f", size = 14808125, upload-time = "2024-09-29T13:41:34.698Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/ca/8f367cb9fe734c4f6f6400e045593beea2635cd736158f9fabf58ee14e3c/pygame-2.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:20349195326a5e82a16e351ed93465a7845a7e2a9af55b7bc1b2110ea3e344e1", size = 13113753, upload-time = "2024-09-29T14:26:13.751Z" },
+ { url = "https://files.pythonhosted.org/packages/83/47/6edf2f890139616b3219be9cfcc8f0cb8f42eb15efd59597927e390538cb/pygame-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3935459109da4bb0b3901da9904f0a3e52028a3332a355d298b1673a334cf21", size = 12378146, upload-time = "2024-09-29T14:26:22.456Z" },
+ { url = "https://files.pythonhosted.org/packages/00/9e/0d8aa8cf93db2d2ee38ebaf1c7b61d0df36ded27eb726221719c150c673d/pygame-2.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31dbdb5d0217f32764797d21c2752e258e5fb7e895326538d82b5f75a0cd856", size = 13611760, upload-time = "2024-09-29T11:10:47.317Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/9e/d06adaa5cc65876bcd7a24f59f67e07f7e4194e6298130024ed3fb22c456/pygame-2.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:173badf82fa198e6888017bea40f511cb28e69ecdd5a72b214e81e4dcd66c3b1", size = 14298054, upload-time = "2024-09-29T11:39:53.891Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/a1/9ae2852ebd3a7cc7d9ae7ff7919ab983e4a5c1b7a14e840732f23b2b48f6/pygame-2.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce8cc108b92de9b149b344ad2e25eedbe773af0dc41dfb24d1f07f679b558c60", size = 13977107, upload-time = "2024-09-29T11:39:56.831Z" },
+ { url = "https://files.pythonhosted.org/packages/31/df/6788fd2e9a864d0496a77670e44a7c012184b7a5382866ab0e60c55c0f28/pygame-2.6.1-cp311-cp311-win32.whl", hash = "sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c", size = 10250863, upload-time = "2024-09-29T11:44:48.199Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/55/ca3eb851aeef4f6f2e98a360c201f0d00bd1ba2eb98e2c7850d80aabc526/pygame-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299", size = 10622016, upload-time = "2024-09-29T12:17:01.545Z" },
+ { url = "https://files.pythonhosted.org/packages/92/16/2c602c332f45ff9526d61f6bd764db5096ff9035433e2172e2d2cadae8db/pygame-2.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ee7f2771f588c966fa2fa8b829be26698c9b4836f82ede5e4edc1a68594942e", size = 13118279, upload-time = "2024-09-29T14:26:30.427Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/53/77ccbc384b251c6e34bfd2e734c638233922449a7844e3c7a11ef91cee39/pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8040ea2ab18c6b255af706ec01355c8a6b08dc48d77fd4ee783f8fc46a843bf", size = 12384524, upload-time = "2024-09-29T14:26:49.996Z" },
+ { url = "https://files.pythonhosted.org/packages/06/be/3ed337583f010696c3b3435e89a74fb29d0c74d0931e8f33c0a4246307a9/pygame-2.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47a6938de93fa610accd4969e638c2aebcb29b2fca518a84c3a39d91ab47116", size = 13587123, upload-time = "2024-09-29T11:10:50.072Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/ca/b015586a450db59313535662991b34d24c1f0c0dc149cc5f496573900f4e/pygame-2.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33006f784e1c7d7e466fcb61d5489da59cc5f7eb098712f792a225df1d4e229d", size = 14275532, upload-time = "2024-09-29T11:39:59.356Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/f2/d31e6ad42d657af07be2ffd779190353f759a07b51232b9e1d724f2cda46/pygame-2.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1206125f14cae22c44565c9d333607f1d9f59487b1f1432945dfc809aeaa3e88", size = 13952653, upload-time = "2024-09-29T11:40:01.781Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/42/8ea2a6979e6fa971702fece1747e862e2256d4a8558fe0da6364dd946c53/pygame-2.6.1-cp312-cp312-win32.whl", hash = "sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e", size = 10252421, upload-time = "2024-09-29T11:14:26.877Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/90/7d766d54bb95939725e9a9361f9c06b0cfbe3fe100aa35400f0a461a278a/pygame-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65", size = 10624591, upload-time = "2024-09-29T11:52:54.489Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/91/718acf3e2a9d08a6ddcc96bd02a6f63c99ee7ba14afeaff2a51c987df0b9/pygame-2.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2", size = 13090765, upload-time = "2024-09-29T14:27:02.377Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/c6/9cb315de851a7682d9c7568a41ea042ee98d668cb8deadc1dafcab6116f0/pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171", size = 12381704, upload-time = "2024-09-29T14:27:10.228Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/8f/617a1196e31ae3b46be6949fbaa95b8c93ce15e0544266198c2266cc1b4d/pygame-2.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b", size = 13581091, upload-time = "2024-09-29T11:30:27.653Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/87/2851a564e40a2dad353f1c6e143465d445dab18a95281f9ea458b94f3608/pygame-2.6.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b", size = 14273844, upload-time = "2024-09-29T11:40:04.138Z" },
+ { url = "https://files.pythonhosted.org/packages/85/b5/aa23aa2e70bcba42c989c02e7228273c30f3b44b9b264abb93eaeff43ad7/pygame-2.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c", size = 13951197, upload-time = "2024-09-29T11:40:06.785Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/06/29e939b34d3f1354738c7d201c51c250ad7abefefaf6f8332d962ff67c4b/pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e", size = 10249309, upload-time = "2024-09-29T11:10:23.329Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084, upload-time = "2024-09-29T11:48:51.587Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
-version = "8.3.5"
+version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
- { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "pygments" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-cov"
-version = "6.1.1"
+version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
{ name = "pytest" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 },
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
@@ -642,141 +784,224 @@ dependencies = [
{ name = "pytest" },
{ name = "trio" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/13/08/056279526554c6c6e6ad6d4a479a338d14dc785ac30be8bdc6ca0153c1be/pytest-trio-0.8.0.tar.gz", hash = "sha256:8363db6336a79e6c53375a2123a41ddbeccc4aa93f93788651641789a56fb52e", size = 46525 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/22/71953f47e0da5852c899f58cd7a31e6100f37c632b7b9ee52d067613a844/pytest_trio-0.8.0-py3-none-any.whl", hash = "sha256:e6a7e7351ae3e8ec3f4564d30ee77d1ec66e1df611226e5618dbb32f9545c841", size = 27221 },
+sdist = { url = "https://files.pythonhosted.org/packages/13/08/056279526554c6c6e6ad6d4a479a338d14dc785ac30be8bdc6ca0153c1be/pytest-trio-0.8.0.tar.gz", hash = "sha256:8363db6336a79e6c53375a2123a41ddbeccc4aa93f93788651641789a56fb52e", size = 46525, upload-time = "2022-11-01T17:24:29.352Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/22/71953f47e0da5852c899f58cd7a31e6100f37c632b7b9ee52d067613a844/pytest_trio-0.8.0-py3-none-any.whl", hash = "sha256:e6a7e7351ae3e8ec3f4564d30ee77d1ec66e1df611226e5618dbb32f9545c841", size = 27221, upload-time = "2022-11-01T17:24:27.501Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "ruff"
-version = "0.11.8"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473 },
- { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862 },
- { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273 },
- { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330 },
- { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223 },
- { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353 },
- { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936 },
- { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083 },
- { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834 },
- { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713 },
- { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182 },
- { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027 },
- { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298 },
- { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884 },
- { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102 },
- { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410 },
- { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129 },
+version = "0.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" },
+ { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" },
+ { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
+ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]]
name = "tomli"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
- { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
- { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
- { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
- { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
- { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
- { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
- { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
- { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
- { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
- { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
- { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
- { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
- { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
- { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
- { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
- { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
- { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
- { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
- { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
- { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
- { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
- { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
- { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
- { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
- { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
- { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
- { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
- { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
- { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
- { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
name = "trio"
-version = "0.30.0"
+version = "0.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "outcome" },
{ name = "sniffio" },
{ name = "sortedcontainers" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776 }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/ce/0041ddd9160aac0031bcf5ab786c7640d795c797e67c438e15cfedf815c8/trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", size = 605323, upload-time = "2025-10-31T07:18:17.466Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194 },
+ { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" },
]
[[package]]
name = "typing-extensions"
-version = "4.13.2"
+version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "uv"
-version = "0.7.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fd/d4/c1104ee4d8a69e4834888cd850eb4f9327c585e5e60da108fda788d3872d/uv-0.7.2.tar.gz", hash = "sha256:45e619bb076916b79df8c5ecc28d1be04d1ccd0b63b080c44ae973b8deb33b25", size = 3293566 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/22/c3/68291a239dbedc0389fa5ce5b5b6c7c2a54c52bc11e9503276f376faa9e7/uv-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:e1e4394b54bc387f227ca1b2aa0348d35f6455b6168ca1826c1dc5f4fc3e8d20", size = 16590159 },
- { url = "https://files.pythonhosted.org/packages/6c/ac/3c7e8df1d6bb84a805aa773ea4f6a006682f8241f331c9c359eb5310f042/uv-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c0edb194c35f1f12c75bec4fe2d7d4d09f0c2cec3a16102217a772620ce1d6e6", size = 16753976 },
- { url = "https://files.pythonhosted.org/packages/42/ca/6a3f3c094794d482e3418f6a46c2753fa4f6ed2fe5b7ecf299db8cfed9ea/uv-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be2e8d033936ba8ed9ccf85eb2d15c7a8db3bb3e9c4960bdf7c3c98034a6dbda", size = 15513631 },
- { url = "https://files.pythonhosted.org/packages/1e/65/6fae29e0eb884fa1cab89b0fa865d409e0e2bcada8316cd50b4c81e8706c/uv-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a314a94b42bc6014f18c877f723292306b76c10b455c2b385728e1470e661ced", size = 15972100 },
- { url = "https://files.pythonhosted.org/packages/a6/92/3d8da1efc7f3272ccc65c50cb13abd9e6a32246bb6c258175c68a91d0d80/uv-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4d1652fe3608fa564dbeaeb2465208f691ac04b57f655ebef62e9ec6d37103d", size = 16288666 },
- { url = "https://files.pythonhosted.org/packages/2c/5e/7d6a788c45d5e2686d01c4886ebb21149892a59bcfa15b66d0646e73aafa/uv-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48c115a3c13c3b29748e325093ee04fd48eaf91145bedc68727f78e6a1c34ab8", size = 17165785 },
- { url = "https://files.pythonhosted.org/packages/e4/9e/4d0a947ffa4b377c6e34935c23164c7914d7239154d254aa5938db6a7e83/uv-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c388172209ca5a47706666d570a45fef3dd39db9258682e10b2f62ca521f0e91", size = 18014800 },
- { url = "https://files.pythonhosted.org/packages/c7/31/781288f9f53e1770128f7830841d7d269097ed70a4afa71578d45721bfa2/uv-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c97cc5e8029a8dc0e1fc39f15f746be931345bc0aeae85feceaa1828f0de87", size = 17745484 },
- { url = "https://files.pythonhosted.org/packages/6d/04/030eec46217225b77ccff1f2808e64074873d86fe445be3784649506e65e/uv-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fa315366ee36ad1f734734f3153e2f334342900061fc0ed18b06f3b9bb2dfe2", size = 22103174 },
- { url = "https://files.pythonhosted.org/packages/5c/07/9d85d0a9ddd49dbec18bde741ffb33d0c671a153461b094a9c73504e1b92/uv-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7236ec776c559fbc3ae4389b7cd506a2428ad9dd0402ac3d9446200ea3dc45f6", size = 17369922 },
- { url = "https://files.pythonhosted.org/packages/11/18/cfef0efe3c4ebdd81422f35215bb915fd599fc946b40306186d87e90678b/uv-0.7.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:78ec372b2f5c7ff8a034e16dd04bc579a62561a5eac4b6dfc96af60298a97d31", size = 16209878 },
- { url = "https://files.pythonhosted.org/packages/31/ed/2ddd7547203ddd368b9ec56b245e09931f868daf2d2b0e29c0b69584466d/uv-0.7.2-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:28fd5d689ae4f8f16533f091a6dd63e1ddf3b7c782003ac8a18584ddb8823cbe", size = 16271878 },
- { url = "https://files.pythonhosted.org/packages/f0/9c/30a48a9d875b91b486286d1a4ccc081dad130acea0dca683c1786ddd7c84/uv-0.7.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9aaacb143622cd437a446a4b316a546c02403b438cd7fd7556d62f47a9fd0a99", size = 16742005 },
- { url = "https://files.pythonhosted.org/packages/a5/b3/5550a721a1e8a99117d960f16c05ad8d39aff79a3fc1aadf2ed13da4385f/uv-0.7.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:81b86fff996c302be6aa1c1ac6eb72b97a7277c319e52c0def50d40b1ffaa617", size = 17443927 },
- { url = "https://files.pythonhosted.org/packages/52/1f/71a7c3e9c79718647fea1e6fe85ccc82d2629cd858b437ae2081190045cc/uv-0.7.2-py3-none-win32.whl", hash = "sha256:19a64c38657c4fbe7c945055755500116fdaac8e121381a5245ea66823f8c500", size = 16869579 },
- { url = "https://files.pythonhosted.org/packages/44/f0/4424cf64533b7576610f7de5c94183d810743b08e81072a2bb2d98316947/uv-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dc1ee6114c824f5880c584a96b2947a35817fdd3a0b752d1adbd926ae6872d1c", size = 18287842 },
- { url = "https://files.pythonhosted.org/packages/0a/5c/12ce48cab21fb0f9bde4ea0c19ec2ab88d4aa9a53e148a52cfb9a41578c9/uv-0.7.2-py3-none-win_arm64.whl", hash = "sha256:0445e56d3f9651ad84d5a7f16efabba83bf305b73594f1c1bc0659aeab952040", size = 16929582 },
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/36/f7fe4de0ad81234ac43938fe39c6ba84595c6b3a1868d786a4d7ad19e670/uv-0.10.0.tar.gz", hash = "sha256:ad01dd614a4bb8eb732da31ade41447026427397c5ad171cc98bd59579ef57ea", size = 3854103, upload-time = "2026-02-05T20:57:55.248Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/69/33fb64aee6ba138b1aaf957e20778e94a8c23732e41cdf68e6176aa2cf4e/uv-0.10.0-py3-none-linux_armv6l.whl", hash = "sha256:38dc0ccbda6377eb94095688c38e5001b8b40dfce14b9654949c1f0b6aa889df", size = 21984662, upload-time = "2026-02-05T20:57:19.076Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/5a/e3ff8a98cfbabc5c2d09bf304d2d9d2d7b2e7d60744241ac5ed762015e5c/uv-0.10.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a165582c1447691109d49d09dccb065d2a23852ff42bf77824ff169909aa85da", size = 21057249, upload-time = "2026-02-05T20:56:48.921Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/77/ec8f24f8d0f19c4fda0718d917bb78b9e6f02a4e1963b401f1c4f4614a54/uv-0.10.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aefea608971f4f23ac3dac2006afb8eb2b2c1a2514f5fee1fac18e6c45fd70c4", size = 19827174, upload-time = "2026-02-05T20:57:10.581Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7e/09b38b93208906728f591f66185a425be3acdb97c448460137d0e6ecb30a/uv-0.10.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:d4b621bcc5d0139502789dc299bae8bf55356d07b95cb4e57e50e2afcc5f43e1", size = 21629522, upload-time = "2026-02-05T20:57:29.959Z" },
+ { url = "https://files.pythonhosted.org/packages/89/f3/48d92c90e869331306979efaa29a44c3e7e8376ae343edc729df0d534dfb/uv-0.10.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:b4bea728a6b64826d0091f95f28de06dd2dc786384b3d336a90297f123b4da0e", size = 21614812, upload-time = "2026-02-05T20:56:58.103Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/43/d0dedfcd4fe6e36cabdbeeb43425cd788604db9d48425e7b659d0f7ba112/uv-0.10.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc0cc2a4bcf9efbff9a57e2aed21c2d4b5a7ec2cc0096e0c33d7b53da17f6a3b", size = 21577072, upload-time = "2026-02-05T20:57:45.455Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/90/b8c9320fd8d86f356e37505a02aa2978ed28f9c63b59f15933e98bce97e5/uv-0.10.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:070ca2f0e8c67ca9a8f70ce403c956b7ed9d51e0c2e9dbbcc4efa5e0a2483f79", size = 22829664, upload-time = "2026-02-05T20:57:22.689Z" },
+ { url = "https://files.pythonhosted.org/packages/56/9c/2c36b30b05c74b2af0e663e0e68f1d10b91a02a145e19b6774c121120c0b/uv-0.10.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8070c66149c06f9b39092a06f593a2241345ea2b1d42badc6f884c2cc089a1b1", size = 23705815, upload-time = "2026-02-05T20:57:37.604Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/a1/8c7fdb14ab72e26ca872e07306e496a6b8cf42353f9bf6251b015be7f535/uv-0.10.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3db1d5390b3a624de672d7b0f9c9d8197693f3b2d3d9c4d9e34686dcbc34197a", size = 22890313, upload-time = "2026-02-05T20:57:26.35Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f8/5c152350b1a6d0af019801f91a1bdeac854c33deb36275f6c934f0113cb5/uv-0.10.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b46db718763bf742e986ebbc7a30ca33648957a0dcad34382970b992f5e900", size = 22769440, upload-time = "2026-02-05T20:56:53.859Z" },
+ { url = "https://files.pythonhosted.org/packages/87/44/980e5399c6f4943b81754be9b7deb87bd56430e035c507984e17267d6a97/uv-0.10.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:eb95d28590edd73b8fdd80c27d699c45c52f8305170c6a90b830caf7f36670a4", size = 21695296, upload-time = "2026-02-05T20:57:06.732Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/e7/f44ad40275be2087b3910df4678ed62cf0c82eeb3375c4a35037a79747db/uv-0.10.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5871eef5046a81df3f1636a3d2b4ccac749c23c7f4d3a4bae5496cb2876a1814", size = 22424291, upload-time = "2026-02-05T20:57:49.067Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/81/31c0c0a8673140756e71a1112bf8f0fcbb48a4cf4587a7937f5bd55256b6/uv-0.10.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:1af0ec125a07edb434dfaa98969f6184c1313dbec2860c3c5ce2d533b257132a", size = 22109479, upload-time = "2026-02-05T20:57:02.258Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/d1/2eb51bc233bad3d13ad64a0c280fd4d1ebebf5c2939b3900a46670fa2b91/uv-0.10.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:45909b9a734250da05b10101e0a067e01ffa2d94bbb07de4b501e3cee4ae0ff3", size = 22972087, upload-time = "2026-02-05T20:57:52.847Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f7/49987207b87b5c21e1f0e81c52892813e8cdf7e318b6373d6585773ebcdd/uv-0.10.0-py3-none-win32.whl", hash = "sha256:d5498851b1f07aa9c9af75578b2029a11743cb933d741f84dcbb43109a968c29", size = 20896746, upload-time = "2026-02-05T20:57:33.426Z" },
+ { url = "https://files.pythonhosted.org/packages/80/b2/1370049596c6ff7fa1fe22fccf86a093982eac81017b8c8aff541d7263b2/uv-0.10.0-py3-none-win_amd64.whl", hash = "sha256:edd469425cd62bcd8c8cc0226c5f9043a94e37ed869da8268c80fdbfd3e5015e", size = 23433041, upload-time = "2026-02-05T20:57:41.41Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/76/1034c46244feafec2c274ac52b094f35d47c94cdb11461c24cf4be8a0c0c/uv-0.10.0-py3-none-win_arm64.whl", hash = "sha256:e90c509749b3422eebb54057434b7119892330d133b9690a88f8a6b0f3116be3", size = 21880261, upload-time = "2026-02-05T20:57:14.724Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.36.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
]
From aa2e337d601ca45c6faba869d4397433e18f4add Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 6 Feb 2026 21:07:13 -0600
Subject: [PATCH 60/67] Ruff formatting
---
README.md | 2 --
src/azul/__init__.py | 1 -
src/azul/database.py | 9 +++---
src/azul/game.py | 5 +--
src/azul/namedtuple_mod.py | 4 ++-
src/azul/state.py | 2 +-
src/azul/statemachine.py | 3 +-
src/azul/vector.py | 3 +-
src/azul_computer_players/MiniMax_AI.py | 3 +-
src/azul_computer_players/machine_client.py | 8 ++---
tests/test_sprite.py | 12 +++----
tests/test_statemachine.py | 36 ++++++++++++++-------
12 files changed, 44 insertions(+), 44 deletions(-)
diff --git a/README.md b/README.md
index a081d6c..a015365 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,6 @@ Graphical Azul Game with AI support
[](https://results.pre-commit.ci/latest/github/CoolCat467/Azul/main)
[](https://github.com/pre-commit/pre-commit)
-[](https://github.com/psf/black)
-[](https://github.com/astral-sh/ruff)
diff --git a/src/azul/__init__.py b/src/azul/__init__.py
index 8e25fb5..55df39e 100644
--- a/src/azul/__init__.py
+++ b/src/azul/__init__.py
@@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-
if __name__ == "__main__":
from azul.game import cli_run as cli_run
diff --git a/src/azul/database.py b/src/azul/database.py
index 8d19b2a..ba95b20 100644
--- a/src/azul/database.py
+++ b/src/azul/database.py
@@ -5,7 +5,7 @@
from __future__ import annotations
# Database - Read and write json files
-# Copyright (C) 2024 CoolCat467
+# Copyright (C) 2024-2026 CoolCat467
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -33,8 +33,7 @@
if TYPE_CHECKING:
from collections.abc import Generator, Iterable, Iterator
from types import TracebackType
-
- from typing_extensions import Self
+ from typing import Self
_LOADED: dict[str, Records] = {}
@@ -283,7 +282,7 @@ def load(file_path: str | Path | trio.Path) -> Records:
async def load_async(file_path: str | Path | trio.Path) -> Records:
"""Load database from file path or return already loaded instance."""
await trio.lowlevel.checkpoint()
- file = path.abspath(file_path)
+ file = await trio.Path.abspath(file_path)
if file not in _LOADED:
_LOADED[file] = Records(file, auto_load=False)
if await trio.Path(file).exists():
@@ -308,7 +307,7 @@ def unload(file_path: str | Path | trio.Path) -> None:
async def async_unload(file_path: str | Path | trio.Path) -> None:
"""If database loaded, write file and unload."""
- file = path.abspath(file_path)
+ file = await trio.Path.abspath(file_path)
if file not in get_loaded():
return
database = load(file)
diff --git a/src/azul/game.py b/src/azul/game.py
index 7339b19..9e1a49f 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -73,9 +73,6 @@
)
from azul.vector import Vector2
-if sys.version_info < (3, 11):
- from exceptiongroup import ExceptionGroup
-
if TYPE_CHECKING:
from collections.abc import (
Awaitable,
@@ -1898,7 +1895,7 @@ async def handle_game_initial_config(
) -> None:
"""Handle `game_initial_config` event."""
(
- variant_play,
+ _variant_play,
player_count,
factory_count,
self.current_turn,
diff --git a/src/azul/namedtuple_mod.py b/src/azul/namedtuple_mod.py
index 21ba1e2..a85f43c 100644
--- a/src/azul/namedtuple_mod.py
+++ b/src/azul/namedtuple_mod.py
@@ -27,7 +27,9 @@ def __new__(
ns: dict[str, typing.Any],
) -> typing.Any: # pragma: nocover
"""Create NamedTuple."""
- bases = tuple(tuple if base is typing._NamedTuple else base for base in bases) # type: ignore[attr-defined]
+ bases = tuple(
+ tuple if base is typing._NamedTuple else base for base in bases
+ ) # type: ignore[attr-defined]
for base in bases:
if tuple not in base.__mro__:
continue
diff --git a/src/azul/state.py b/src/azul/state.py
index 6514ac0..d19934f 100644
--- a/src/azul/state.py
+++ b/src/azul/state.py
@@ -40,9 +40,9 @@
if TYPE_CHECKING:
from collections.abc import Generator
+ from typing import Self
from numpy.typing import NDArray
- from typing_extensions import Self
T = TypeVar("T")
diff --git a/src/azul/statemachine.py b/src/azul/statemachine.py
index 9c37448..6d24c0a 100644
--- a/src/azul/statemachine.py
+++ b/src/azul/statemachine.py
@@ -30,8 +30,7 @@
if TYPE_CHECKING:
from collections.abc import Iterable
-
- from typing_extensions import Self
+ from typing import Self
import trio
__all__ = ["AsyncState", "AsyncStateMachine", "State", "StateMachine"]
diff --git a/src/azul/vector.py b/src/azul/vector.py
index 60fb30e..0c2e5b4 100644
--- a/src/azul/vector.py
+++ b/src/azul/vector.py
@@ -36,8 +36,7 @@
if TYPE_CHECKING:
from collections.abc import Generator, Iterable, Iterator
-
- from typing_extensions import Self
+ from typing import Self
# As a forward to the madness below, we are doing something incredibly sneeky.
# We have BaseVector, which we want to have all of the shared functionality
diff --git a/src/azul_computer_players/MiniMax_AI.py b/src/azul_computer_players/MiniMax_AI.py
index 2e290a8..223347a 100755
--- a/src/azul_computer_players/MiniMax_AI.py
+++ b/src/azul_computer_players/MiniMax_AI.py
@@ -27,8 +27,7 @@
if TYPE_CHECKING:
from collections.abc import Iterable
-
- from typing_extensions import Self
+ from typing import Self
T = TypeVar("T")
Action: TypeAlias = (
diff --git a/src/azul_computer_players/machine_client.py b/src/azul_computer_players/machine_client.py
index 69fa89d..b4fd4f6 100644
--- a/src/azul_computer_players/machine_client.py
+++ b/src/azul_computer_players/machine_client.py
@@ -6,7 +6,6 @@
__author__ = "CoolCat467"
__version__ = "0.0.0"
-import sys
from abc import ABCMeta, abstractmethod
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, TypeAlias, cast
@@ -43,9 +42,6 @@
from numpy.typing import NDArray
-if sys.version_info < (3, 11):
- from exceptiongroup import BaseExceptionGroup
-
# Player:
# 0 = False = Person = MIN = 0, 2
# 1 = True = AI (Us) = MAX = 1, 3
@@ -238,9 +234,9 @@ async def handle_initial_config(
(
variant_play,
player_count,
- factory_count,
+ _factory_count,
current_turn,
- floor_line_data,
+ _floor_line_data,
) = event.data
## print(f'[RemoteState] {variant_play = }')
self.state = State.new_game(player_count, bool(variant_play))
diff --git a/tests/test_sprite.py b/tests/test_sprite.py
index 1344fc2..00a14b4 100644
--- a/tests/test_sprite.py
+++ b/tests/test_sprite.py
@@ -150,7 +150,7 @@ def test_image_component_get_image_fail(
) -> None:
with pytest.raises(
ValueError,
- match='^No image saved for identifier "test_image"$',
+ match=r'^No image saved for identifier "test_image"$',
):
image_component.get_image("test_image")
@@ -160,7 +160,7 @@ def test_image_component_get_mask_fail(
) -> None:
with pytest.raises(
ValueError,
- match='^No mask saved for identifier "test_mask"$',
+ match=r'^No mask saved for identifier "test_mask"$',
):
image_component.get_mask("test_mask")
@@ -178,12 +178,12 @@ def test_image_component_add_image_and_mask_invalid_image(
) -> None:
with pytest.raises(
ValueError,
- match="^Expected surface to be a valid identifier$",
+ match=r"^Expected surface to be a valid identifier$",
):
image_component.add_image_and_mask("test_image", None, None) # type: ignore[arg-type]
with pytest.raises(
ValueError,
- match="^Expected surface to be a valid identifier$",
+ match=r"^Expected surface to be a valid identifier$",
):
image_component.add_image_and_mask("test_image", "copy_from", None) # type: ignore[arg-type]
@@ -194,12 +194,12 @@ def test_image_component_add_image_and_mask_invalid_mask(
image = Surface((1, 1))
with pytest.raises(
ValueError,
- match="^Expected mask to be a valid identifier$",
+ match=r"^Expected mask to be a valid identifier$",
):
image_component.add_image_and_mask("test_image", image, None) # type: ignore[arg-type]
with pytest.raises(
ValueError,
- match="^Expected mask to be a valid identifier$",
+ match=r"^Expected mask to be a valid identifier$",
):
image_component.add_image_and_mask("test_image", image, "copy_from") # type: ignore[arg-type]
diff --git a/tests/test_statemachine.py b/tests/test_statemachine.py
index f3cfafe..f5c34b0 100644
--- a/tests/test_statemachine.py
+++ b/tests/test_statemachine.py
@@ -17,7 +17,7 @@ def test_state() -> None:
with pytest.raises(
RuntimeError,
- match="^State has no statemachine bound$",
+ match=r"^State has no statemachine bound$",
):
print(state.machine)
@@ -34,7 +34,7 @@ def test_async_state() -> None:
with pytest.raises(
RuntimeError,
- match="^State has no statemachine bound$",
+ match=r"^State has no statemachine bound$",
):
print(state.machine)
@@ -50,11 +50,14 @@ def add_actions(self) -> None:
machine.add_state(TestState("test"))
bob = State("bob")
- with pytest.raises(RuntimeError, match="State has no statemachine bound"):
+ with pytest.raises(
+ RuntimeError,
+ match=r"^State has no statemachine bound$",
+ ):
assert bob.machine
- with pytest.raises(TypeError, match="is not an instance of State!"):
+ with pytest.raises(TypeError, match=r"is not an instance of State!$"):
machine.add_state(AsyncState("test"))
- with pytest.raises(ValueError, match="is not a registered State."):
+ with pytest.raises(ValueError, match=r"is not a registered State\.$"):
machine.remove_state("waffle")
machine.add_state(bob)
assert add_actions_run
@@ -66,7 +69,10 @@ def add_actions(self) -> None:
for _ in range(3):
gc.collect()
- with pytest.raises(RuntimeError, match="State has no statemachine bound"):
+ with pytest.raises(
+ RuntimeError,
+ match=r"^State has no statemachine bound$",
+ ):
assert bob.machine
@@ -85,7 +91,7 @@ def test_state_machine_think() -> None:
machine.remove_state("jerald")
machine.remove_state("bob")
machine.set_state(None)
- with pytest.raises(KeyError, match="not found in internal states"):
+ with pytest.raises(KeyError, match=r"not found in internal states$"):
machine.set_state("bob")
machine.add_state(State("bob"))
@@ -106,11 +112,14 @@ async def test_async_state_machine_add() -> None:
machine.add_state(AsyncState("test"))
bob = AsyncState("bob")
- with pytest.raises(RuntimeError, match="State has no statemachine bound"):
+ with pytest.raises(
+ RuntimeError,
+ match=r"^State has no statemachine bound$",
+ ):
assert bob.machine
- with pytest.raises(TypeError, match="is not an instance of AsyncState!"):
+ with pytest.raises(TypeError, match=r"is not an instance of AsyncState!$"):
machine.add_state(State("test"))
- with pytest.raises(ValueError, match="is not a registered AsyncState."):
+ with pytest.raises(ValueError, match=r"is not a registered AsyncState\.$"):
machine.remove_state("waffle")
machine.add_state(bob)
@@ -121,7 +130,10 @@ async def test_async_state_machine_add() -> None:
for _ in range(3):
gc.collect()
- with pytest.raises(RuntimeError, match="State has no statemachine bound"):
+ with pytest.raises(
+ RuntimeError,
+ match=r"^State has no statemachine bound$",
+ ):
assert bob.machine
@@ -141,7 +153,7 @@ async def test_async_state_machine_think() -> None:
await jerald.exit_actions()
machine.remove_state("bob")
await machine.set_state(None)
- with pytest.raises(KeyError, match="not found in internal states"):
+ with pytest.raises(KeyError, match=r"not found in internal states$"):
await machine.set_state("bob")
machine.add_state(AsyncState("bob"))
From 9171247b9582cf423298c3d2140badf882907d90 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 6 Feb 2026 21:28:48 -0600
Subject: [PATCH 61/67] Updates from checkers
---
src/azul/database.py | 4 +-
src/azul/element_list.py | 2 +-
src/azul/errorbox.py | 1 -
src/azul/game.py | 2 +-
src/azul/namedtuple_mod.py | 9 +--
src/azul/sprite.py | 6 +-
src/azul/vector.py | 1 +
src/azul_computer_players/MiniMax_AI.py | 4 +-
src/azul_computer_players/machine_client.py | 2 +-
tests/test_objects.py | 23 +++++---
tests/test_sprite.py | 22 +++++---
tests/test_statemachine.py | 62 +++++++++------------
tests/test_vector.py | 6 +-
13 files changed, 71 insertions(+), 73 deletions(-)
diff --git a/src/azul/database.py b/src/azul/database.py
index ba95b20..fcc1755 100644
--- a/src/azul/database.py
+++ b/src/azul/database.py
@@ -282,7 +282,7 @@ def load(file_path: str | Path | trio.Path) -> Records:
async def load_async(file_path: str | Path | trio.Path) -> Records:
"""Load database from file path or return already loaded instance."""
await trio.lowlevel.checkpoint()
- file = await trio.Path.abspath(file_path)
+ file = str(await trio.Path(file_path).absolute())
if file not in _LOADED:
_LOADED[file] = Records(file, auto_load=False)
if await trio.Path(file).exists():
@@ -307,7 +307,7 @@ def unload(file_path: str | Path | trio.Path) -> None:
async def async_unload(file_path: str | Path | trio.Path) -> None:
"""If database loaded, write file and unload."""
- file = await trio.Path.abspath(file_path)
+ file = str(await trio.Path(file_path).absolute())
if file not in get_loaded():
return
database = load(file)
diff --git a/src/azul/element_list.py b/src/azul/element_list.py
index d461aec..665e8c8 100644
--- a/src/azul/element_list.py
+++ b/src/azul/element_list.py
@@ -66,7 +66,7 @@ def __init__(self, name: object) -> None:
def add_element(self, element: Element) -> None:
"""Add element to this list."""
group = self.groups()[-1]
- group.add(element) # type: ignore[arg-type]
+ group.add(element)
self.add_component(element)
self._order.append(element.name)
diff --git a/src/azul/errorbox.py b/src/azul/errorbox.py
index 3fcb40c..5b12b41 100644
--- a/src/azul/errorbox.py
+++ b/src/azul/errorbox.py
@@ -70,7 +70,6 @@ def __tkinter(title: str, message: str) -> None:
from tkinter import messagebox
tk.Tk().wm_withdraw()
- # types: attr-defined error: Module has no attribute "messagebox"
messagebox.showerror(title, message)
diff --git a/src/azul/game.py b/src/azul/game.py
index 9e1a49f..5e1ae2e 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -2092,7 +2092,7 @@ async def async_run() -> None:
## try:
async with trio.open_nursery() as main_nursery:
event_manager = ExternalRaiseManager(
- "checkers",
+ "azul",
main_nursery, # "client"
)
client = AzulClient(event_manager)
diff --git a/src/azul/namedtuple_mod.py b/src/azul/namedtuple_mod.py
index a85f43c..594dc25 100644
--- a/src/azul/namedtuple_mod.py
+++ b/src/azul/namedtuple_mod.py
@@ -28,8 +28,9 @@ def __new__(
) -> typing.Any: # pragma: nocover
"""Create NamedTuple."""
bases = tuple(
- tuple if base is typing._NamedTuple else base for base in bases
- ) # type: ignore[attr-defined]
+ tuple if base is typing._NamedTuple else base # type: ignore[attr-defined]
+ for base in bases
+ )
for base in bases:
if tuple not in base.__mro__:
continue
@@ -55,7 +56,7 @@ def __new__(
module=ns["__module__"],
)
nm_tpl.__bases__ = bases
- if typing.Generic in bases: # type: ignore[comparison-overlap]
+ if typing.Generic in bases:
class_getitem = typing._generic_class_getitem # type: ignore[attr-defined]
nm_tpl.__class_getitem__ = classmethod(class_getitem)
# update from user namespace without overriding special namedtuple attributes
@@ -66,7 +67,7 @@ def __new__(
)
if key not in typing._special and key not in nm_tpl._fields: # type: ignore[attr-defined]
setattr(nm_tpl, key, ns[key])
- if typing.Generic in bases: # type: ignore[comparison-overlap]
+ if typing.Generic in bases:
nm_tpl.__init_subclass__()
return nm_tpl
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index 7d9dd20..083af56 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -99,13 +99,9 @@ def _set_location(self, value: tuple[int, int]) -> None:
"""Set rect center from tuple of integers."""
self.rect.center = value
- def __set_location(self, value: tuple[int, int]) -> None:
- """Set rect center from tuple of integers."""
- self._set_location(value)
-
location = property(
__get_location,
- __set_location,
+ _set_location,
doc="Location (Center of image)",
)
diff --git a/src/azul/vector.py b/src/azul/vector.py
index 0c2e5b4..d6c7e17 100644
--- a/src/azul/vector.py
+++ b/src/azul/vector.py
@@ -75,6 +75,7 @@ class BaseVector:
# typing issues
def __iter__(self) -> Iterator[float]: ... # noqa: D105
def __getitem__(self, value: int) -> float: ... # noqa: D105
+ def __len__(self) -> int: ... # noqa: D105
def _asdict(self) -> dict[str, float]: ...
def _replace(self, /, **kwds: int | float) -> Self: ...
def __getnewargs__(self) -> tuple[float, ...]: ... # noqa: D105
diff --git a/src/azul_computer_players/MiniMax_AI.py b/src/azul_computer_players/MiniMax_AI.py
index 223347a..88cdf0d 100755
--- a/src/azul_computer_players/MiniMax_AI.py
+++ b/src/azul_computer_players/MiniMax_AI.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
-# AI that plays checkers.
+# AI that plays azul.
-"""Minimax Checkers AI."""
+"""Minimax Azul AI."""
from __future__ import annotations
diff --git a/src/azul_computer_players/machine_client.py b/src/azul_computer_players/machine_client.py
index b4fd4f6..88b0602 100644
--- a/src/azul_computer_players/machine_client.py
+++ b/src/azul_computer_players/machine_client.py
@@ -433,7 +433,7 @@ async def run_client(
"""Run machine client and raise tick events."""
async with trio.open_nursery() as main_nursery:
event_manager = ExternalRaiseManager(
- "checkers",
+ "azul",
main_nursery,
"client",
)
diff --git a/tests/test_objects.py b/tests/test_objects.py
index 12e2b56..66450f6 100644
--- a/tests/test_objects.py
+++ b/tests/test_objects.py
@@ -1,10 +1,15 @@
from __future__ import annotations
+from typing import TYPE_CHECKING
+
import pytest
from pygame.surface import Surface
from azul.objects import Button, OutlinedText, Text
+if TYPE_CHECKING:
+ from pygame.font import Font
+
class MockSurface(Surface):
"""Mocking a pygame surface for testing."""
@@ -26,7 +31,7 @@ def render(
text: str,
antialias: bool,
color: tuple[int, int, int],
- ) -> str:
+ ) -> MockSurface:
"""Fake render method."""
return MockSurface(text)
@@ -36,46 +41,46 @@ def font() -> MockFont:
return MockFont()
-def test_text_initialization(font: MockFont) -> None:
+def test_text_initialization(font: Font) -> None:
text = Text("TestText", font)
assert text.text == "None"
assert text.color == (255, 255, 255)
assert text.font == font
-def test_text_rendering(font: MockFont) -> None:
+def test_text_rendering(font: Font) -> None:
text = Text("TestText", font)
assert text.image is None
-def test_text_rendering_blank(font: MockFont) -> None:
+def test_text_rendering_blank(font: Font) -> None:
text = Text("TestText", font)
text.text = ""
text.text = ""
assert text.image.text_data == ""
-def test_outlined_text_initialization(font: MockFont) -> None:
+def test_outlined_text_initialization(font: Font) -> None:
outlined_text = OutlinedText("TestOutlinedText", font)
assert outlined_text.outline == (0, 0, 0)
assert outlined_text.inside == (255, 255, 255)
-def test_outlined_text_rendering(font: MockFont) -> None:
+def test_outlined_text_rendering(font: Font) -> None:
outlined_text = OutlinedText("TestOutlinedText", font)
outlined_text.text = "Outlined Text"
assert outlined_text.text == "Outlined Text"
-def test_outlined_text_rendering_zero_border(font: MockFont) -> None:
+def test_outlined_text_rendering_zero_border(font: Font) -> None:
outlined_text = OutlinedText("TestOutlinedText", font)
outlined_text.border_width = 0
outlined_text.text = "Outlined Text"
assert isinstance(outlined_text.image, Surface)
-def test_button_initialization(font: MockFont) -> None:
+def test_button_initialization(font: Font) -> None:
button = Button("TestButton", font)
assert button.text == "None"
- assert button.color == (0, 0, 0, 255)
+ assert button.color == (0, 0, 0, 255) # type: ignore[comparison-overlap]
assert button.border_width == 3
diff --git a/tests/test_sprite.py b/tests/test_sprite.py
index 00a14b4..14585db 100644
--- a/tests/test_sprite.py
+++ b/tests/test_sprite.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import cast
+from typing import TYPE_CHECKING, cast
import pytest
import trio
@@ -31,24 +31,26 @@ def sprite() -> Sprite:
@pytest.fixture
def image_component(sprite: Sprite) -> ImageComponent:
sprite.add_component(ImageComponent())
- return sprite.get_component("image")
+ return cast("ImageComponent", sprite.get_component("image"))
@pytest.fixture
def animation_component(image_component: ImageComponent) -> AnimationComponent:
- image_component.add_component(AnimationComponent())
- return image_component.get_component("animation")
+ return cast(
+ "AnimationComponent",
+ image_component.get_component("animation"),
+ )
@pytest.fixture
def outline_component(image_component: ImageComponent) -> OutlineComponent:
- return image_component.get_component("outline")
+ return cast("OutlineComponent", image_component.get_component("outline"))
@pytest.fixture
def movement_component(sprite: Sprite) -> MovementComponent:
sprite.add_component(MovementComponent())
- return sprite.get_component("movement")
+ return cast("MovementComponent", sprite.get_component("movement"))
@pytest.fixture
@@ -57,7 +59,7 @@ def targeting_component(
) -> TargetingComponent:
sprite = movement_component.manager
sprite.add_component(TargetingComponent())
- return sprite.get_component("targeting")
+ return cast("TargetingComponent", sprite.get_component("targeting"))
@pytest.fixture
@@ -92,7 +94,7 @@ def test_sprite_image(sprite: Sprite) -> None:
sprite.image = Surface((10, 10))
assert isinstance(sprite.image, Surface)
assert sprite.dirty
- assert sprite.rect.size == (10, 10)
+ assert sprite.rect.size == (10, 10) # type: ignore[unreachable]
def test_sprite_image_set_none(sprite: Sprite) -> None:
@@ -201,7 +203,7 @@ def test_image_component_add_image_and_mask_invalid_mask(
ValueError,
match=r"^Expected mask to be a valid identifier$",
):
- image_component.add_image_and_mask("test_image", image, "copy_from") # type: ignore[arg-type]
+ image_component.add_image_and_mask("test_image", image, "copy_from")
def test_image_component_get_image(image_component: ImageComponent) -> None:
@@ -236,6 +238,8 @@ def test_image_component_set_image_affects_sprite(
image_component.add_image("test_image", image)
assert sprite.image is None
image_component.set_image("test_image")
+ if TYPE_CHECKING:
+ sprite.image = image
assert sprite.image is image
image_component.set_image("test_image")
assert sprite.image is image
diff --git a/tests/test_statemachine.py b/tests/test_statemachine.py
index f5c34b0..6b0cf59 100644
--- a/tests/test_statemachine.py
+++ b/tests/test_statemachine.py
@@ -11,7 +11,7 @@
def test_state() -> None:
- state = State("waffle_time")
+ state = State[StateMachine]("waffle_time")
assert state.name == "waffle_time"
@@ -23,12 +23,12 @@ def test_state() -> None:
def test_state_repr() -> None:
- state = State("waffle_time")
+ state = State[StateMachine]("waffle_time")
assert repr(state) == "State('waffle_time')"
def test_async_state() -> None:
- state = AsyncState("waffle_time")
+ state = AsyncState[AsyncStateMachine]("waffle_time")
assert state.name == "waffle_time"
@@ -43,21 +43,18 @@ def test_state_machine_add() -> None:
machine = StateMachine()
add_actions_run = False
- class TestState(State):
+ class TestState(State[StateMachine]):
def add_actions(self) -> None:
nonlocal add_actions_run
add_actions_run = True
machine.add_state(TestState("test"))
- bob = State("bob")
- with pytest.raises(
- RuntimeError,
- match=r"^State has no statemachine bound$",
- ):
- assert bob.machine
- with pytest.raises(TypeError, match=r"is not an instance of State!$"):
- machine.add_state(AsyncState("test"))
- with pytest.raises(ValueError, match=r"is not a registered State\.$"):
+ bob = State[StateMachine]("bob")
+ with pytest.raises(RuntimeError, match="State has no statemachine bound"):
+ assert bob.machine is not None
+ with pytest.raises(TypeError, match="is not an instance of State!"):
+ machine.add_state(AsyncState("test")) # type: ignore[arg-type]
+ with pytest.raises(ValueError, match=r"is not a registered State\."):
machine.remove_state("waffle")
machine.add_state(bob)
assert add_actions_run
@@ -69,11 +66,8 @@ def add_actions(self) -> None:
for _ in range(3):
gc.collect()
- with pytest.raises(
- RuntimeError,
- match=r"^State has no statemachine bound$",
- ):
- assert bob.machine
+ with pytest.raises(RuntimeError, match="State has no statemachine bound"):
+ assert bob.machine is not None
def test_state_machine_think() -> None:
@@ -91,11 +85,11 @@ def test_state_machine_think() -> None:
machine.remove_state("jerald")
machine.remove_state("bob")
machine.set_state(None)
- with pytest.raises(KeyError, match=r"not found in internal states$"):
+ with pytest.raises(KeyError, match="not found in internal states"):
machine.set_state("bob")
machine.add_state(State("bob"))
- class ToBob(State):
+ class ToBob(State[StateMachine]):
__slots__ = ()
def check_conditions(self) -> str:
@@ -111,15 +105,12 @@ async def test_async_state_machine_add() -> None:
machine = AsyncStateMachine()
machine.add_state(AsyncState("test"))
- bob = AsyncState("bob")
- with pytest.raises(
- RuntimeError,
- match=r"^State has no statemachine bound$",
- ):
- assert bob.machine
- with pytest.raises(TypeError, match=r"is not an instance of AsyncState!$"):
- machine.add_state(State("test"))
- with pytest.raises(ValueError, match=r"is not a registered AsyncState\.$"):
+ bob = AsyncState[AsyncStateMachine]("bob")
+ with pytest.raises(RuntimeError, match="State has no statemachine bound"):
+ assert bob.machine is not None
+ with pytest.raises(TypeError, match="is not an instance of AsyncState!"):
+ machine.add_state(State("test")) # type: ignore[arg-type]
+ with pytest.raises(ValueError, match=r"is not a registered AsyncState\."):
machine.remove_state("waffle")
machine.add_state(bob)
@@ -130,11 +121,8 @@ async def test_async_state_machine_add() -> None:
for _ in range(3):
gc.collect()
- with pytest.raises(
- RuntimeError,
- match=r"^State has no statemachine bound$",
- ):
- assert bob.machine
+ with pytest.raises(RuntimeError, match="State has no statemachine bound"):
+ assert bob.machine is not None
@pytest.mark.trio
@@ -142,7 +130,7 @@ async def test_async_state_machine_think() -> None:
machine = AsyncStateMachine()
await machine.think()
machine.add_states(())
- jerald = AsyncState("jerald")
+ jerald = AsyncState[AsyncStateMachine]("jerald")
machine.add_states((jerald,))
machine.add_state(AsyncState("bob"))
await machine.set_state("jerald")
@@ -153,11 +141,11 @@ async def test_async_state_machine_think() -> None:
await jerald.exit_actions()
machine.remove_state("bob")
await machine.set_state(None)
- with pytest.raises(KeyError, match=r"not found in internal states$"):
+ with pytest.raises(KeyError, match="not found in internal states"):
await machine.set_state("bob")
machine.add_state(AsyncState("bob"))
- class ToBob(AsyncState):
+ class ToBob(AsyncState[AsyncStateMachine]):
__slots__ = ()
async def check_conditions(self) -> str:
diff --git a/tests/test_vector.py b/tests/test_vector.py
index c835d63..230cbf7 100644
--- a/tests/test_vector.py
+++ b/tests/test_vector.py
@@ -28,7 +28,7 @@ def test_eq_vec() -> None:
def test_eq_tuple() -> None:
- assert Vector2(3, 6) == (3, 6)
+ assert Vector2(3, 6) == (3, 6) # type: ignore[comparison-overlap]
def test_from_points() -> None:
@@ -67,6 +67,10 @@ def test_mul() -> None:
assert Vector2(5, 10) * 3 == Vector2(15, 30)
+def test_rmul() -> None:
+ assert 3 * Vector2(5, 10) == Vector2(15, 30)
+
+
def test_truediv() -> None:
assert Vector2(10, 5) / 2 == Vector2(5, 2.5)
From d6c8392f46aec02b708a5aaabc474647e78b4a62 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 6 Feb 2026 22:27:44 -0600
Subject: [PATCH 62/67] Minimax improvements from checkers
---
src/azul/server.py | 3 +-
src/azul_computer_players/MiniMax_AI.py | 230 +++++++++++++++++++++++-
src/azul_computer_players/__init__.py | 17 ++
src/azul_computer_players/__main__.py | 31 ++++
src/azul_computer_players/py.typed | 0
5 files changed, 271 insertions(+), 10 deletions(-)
create mode 100644 src/azul_computer_players/__init__.py
create mode 100644 src/azul_computer_players/__main__.py
create mode 100644 src/azul_computer_players/py.typed
diff --git a/src/azul/server.py b/src/azul/server.py
index 9b3697e..a569bf6 100755
--- a/src/azul/server.py
+++ b/src/azul/server.py
@@ -7,7 +7,7 @@
from __future__ import annotations
-# Copyright (C) 2023-2024 CoolCat467
+# Copyright (C) 2023-2026 CoolCat467
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -25,7 +25,6 @@
__title__ = "Server"
__author__ = "CoolCat467"
__license__ = "GNU General Public License Version 3"
-__version__ = "0.0.0"
import traceback
from collections import deque
diff --git a/src/azul_computer_players/MiniMax_AI.py b/src/azul_computer_players/MiniMax_AI.py
index 88cdf0d..ce33d73 100755
--- a/src/azul_computer_players/MiniMax_AI.py
+++ b/src/azul_computer_players/MiniMax_AI.py
@@ -11,12 +11,13 @@
__author__ = "CoolCat467"
__version__ = "0.0.0"
+import time
+from enum import IntEnum, auto
from math import inf as infinity
-from typing import TYPE_CHECKING, TypeAlias, TypeVar
+from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias, TypeVar
from machine_client import RemoteState, run_clients_in_local_servers_sync
from minimax import Minimax, MinimaxResult, Player
-from mypy_extensions import u8
from azul.state import (
Phase,
@@ -29,15 +30,21 @@
from collections.abc import Iterable
from typing import Self
+ from mypy_extensions import u8
+
T = TypeVar("T")
Action: TypeAlias = (
tuple[SelectableDestinationTiles, ...]
| tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]]
)
-# Player:
-# 0 = False = Person = MIN = 0, 2
-# 1 = True = AI (Us) = MAX = 1, 3
+
+class TranspositionFlag(IntEnum):
+ """Flag enum for transposition table."""
+
+ LOWERBOUND = 0
+ EXACT = auto()
+ UPPERBOUND = auto()
class AutoWallState(State):
@@ -57,11 +64,212 @@ def _factory_offer_maybe_next_turn(self) -> Self:
return new_state
-class AzulMinimax(Minimax[tuple[AutoWallState, u8], Action]):
- """Minimax Algorithm for Checkers."""
+class MinimaxWithID(Minimax[AutoWallState, Action]):
+ """Minimax with ID."""
+
+ __slots__ = ()
+
+ # Simple Transposition Table:
+ # key -> (stored_depth, value, action, flag)
+ # flag: TranspositionFlag: EXACT, LOWERBOUND, UPPERBOUND
+ TRANSPOSITION_TABLE: ClassVar[
+ dict[int, tuple[int, MinimaxResult[Any], TranspositionFlag]]
+ ] = {}
+
+ @classmethod
+ def _transposition_table_lookup(
+ cls,
+ state_hash: int,
+ depth: int,
+ alpha: float,
+ beta: float,
+ ) -> MinimaxResult[Action] | None:
+ """Lookup in transposition_table. Return (value, action) or None."""
+ entry = cls.TRANSPOSITION_TABLE.get(state_hash)
+ if entry is None:
+ return None
+
+ stored_depth, result, flag = entry
+ # only use if stored depth is deep enough
+ if stored_depth >= depth and (
+ (flag == TranspositionFlag.EXACT)
+ or (flag == TranspositionFlag.LOWERBOUND and result.value > alpha)
+ or (flag == TranspositionFlag.UPPERBOUND and result.value < beta)
+ ):
+ return result
+ return None
+
+ @classmethod
+ def _transposition_table_store(
+ cls,
+ state_hash: int,
+ depth: int,
+ result: MinimaxResult[Action],
+ alpha: float,
+ beta: float,
+ ) -> None:
+ """Store in transposition_table with proper flag."""
+ if result.value <= alpha:
+ flag = TranspositionFlag.UPPERBOUND
+ elif result.value >= beta:
+ flag = TranspositionFlag.LOWERBOUND
+ else:
+ flag = TranspositionFlag.EXACT
+ cls.TRANSPOSITION_TABLE[state_hash] = (depth, result, flag)
+
+ @classmethod
+ def hash_state(cls, state: State) -> int:
+ """Your state-to-hash function. Must be consistent."""
+ # For small games you might do: return hash(state)
+ # For larger, use Zobrist or custom.
+ return hash(state)
+
+ @classmethod
+ def alphabeta_transposition_table(
+ cls,
+ state: State,
+ depth: int = 5,
+ a: int | float = -infinity,
+ b: int | float = infinity,
+ ) -> MinimaxResult[Action]:
+ """AlphaBeta with transposition table."""
+ if cls.terminal(state):
+ return MinimaxResult(cls.value(state), None)
+ if depth <= 0:
+ ## # Choose a random action
+ ## # No need for cryptographic secure random
+ return MinimaxResult(
+ cls.value(state),
+ next(iter(cls.actions(state))),
+ )
+ next_down = depth - 1
+
+ state_h = cls.hash_state(state)
+ # 1) Try transposition_table lookup
+ transposition_table_hit = cls._transposition_table_lookup(
+ state_h,
+ depth,
+ a,
+ b,
+ )
+ if transposition_table_hit is not None:
+ return transposition_table_hit
+ next_down = None if depth is None else depth - 1
+
+ current_player = cls.player(state)
+ value: int | float
+
+ best_action: Action | None = None
+
+ if current_player == Player.MAX:
+ value = -infinity
+ actions: list[tuple[Action, State]] = [
+ (action, cls.result(state, action))
+ for action in cls.actions(state)
+ ]
+
+ actions.sort(key=lambda act: cls.value(act[1]), reverse=True)
+ for action, next_state in actions:
+ child = cls.alphabeta_transposition_table(
+ next_state,
+ next_down,
+ a,
+ b,
+ )
+ if child.value > value:
+ value = child.value
+ best_action = action
+ a = max(a, value)
+ if a >= b:
+ break
+
+ elif current_player == Player.MIN:
+ value = infinity
+ actions = [
+ (action, cls.result(state, action))
+ for action in cls.actions(state)
+ ]
+
+ actions.sort(key=lambda act: cls.value(act[1]))
+ for action, next_state in actions:
+ child = cls.alphabeta_transposition_table(
+ next_state,
+ next_down,
+ a,
+ b,
+ )
+ if child.value < value:
+ value = child.value
+ best_action = action
+ b = min(b, value)
+ if b <= a:
+ break
+ else:
+ raise NotImplementedError(f"{current_player = }")
+
+ # 2) Store in transposition_table
+ result = MinimaxResult(value, best_action)
+ cls._transposition_table_store(state_h, depth, result, a, b)
+ return result
+
+ @classmethod
+ def iterative_deepening(
+ cls,
+ state: State,
+ start_depth: int = 5,
+ max_depth: int = 7,
+ time_limit_ns: int | float | None = None,
+ ) -> MinimaxResult[Action]:
+ """Run alpha-beta with increasing depth up to max_depth.
+
+ If time_limit_ns is None, do all depths. Otherwise stop early.
+ """
+ best_result: MinimaxResult[Action] = MinimaxResult(0, None)
+ start_t = time.perf_counter_ns()
+
+ for depth in range(start_depth, max_depth + 1):
+ # clear or keep transposition_table between depths? often you keep it
+ # cls.TRANSPOSITION_TABLE.clear()
+
+ result = cls.alphabeta_transposition_table(
+ state,
+ depth,
+ )
+ best_result = result
+
+ if abs(result.value) == cls.HIGHEST:
+ print(f"reached terminal state stop {depth=}")
+ break
+
+ # optional time check
+ if (
+ time_limit_ns
+ and (time.perf_counter_ns() - start_t) > time_limit_ns
+ ):
+ print(
+ f"break from time expired {depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)",
+ )
+ break
+ print(
+ f"{depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)",
+ )
+
+ return best_result
+
+
+# Minimax[tuple[AutoWallState, u8], Action]
+class AzulMinimax(MinimaxWithID):
+ """Minimax Algorithm for Azul."""
__slots__ = ()
+ ## @classmethod
+ ## def hash_state(cls, state: AutoWallState) -> int:
+ ## """Return state hash value."""
+ ## # For small games you might do: return hash(state)
+ ## # For larger, use Zobrist or custom.
+ ## return hash((state.size, tuple(state.pieces.items()), state.turn))
+
@staticmethod
def value(state: tuple[AutoWallState, u8]) -> int | float:
"""Return value of given game state."""
@@ -165,8 +373,14 @@ async def preform_turn(self) -> Action:
##)
##value, action = CheckersMinimax.minimax(self.state, 4)
assert isinstance(self.state, AutoWallState)
- value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 2)
+ ## value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 2)
## value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 4)
+ value, action = AzulMinimax.iterative_deepening(
+ self.state,
+ 2,
+ 6,
+ int(5 * 1e9),
+ )
if action is None:
raise ValueError("action is None")
print(f"{value = }")
diff --git a/src/azul_computer_players/__init__.py b/src/azul_computer_players/__init__.py
new file mode 100644
index 0000000..63513af
--- /dev/null
+++ b/src/azul_computer_players/__init__.py
@@ -0,0 +1,17 @@
+"""Azul Computer Players."""
+
+# Azul Computer Players
+# Copyright (C) 2026 CoolCat467
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
diff --git a/src/azul_computer_players/__main__.py b/src/azul_computer_players/__main__.py
new file mode 100644
index 0000000..a551a72
--- /dev/null
+++ b/src/azul_computer_players/__main__.py
@@ -0,0 +1,31 @@
+"""Computer Players Module."""
+
+# Programmed by CoolCat467
+
+from __future__ import annotations
+
+# Computer Players Module
+# Copyright (C) 2026 CoolCat467
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+__title__ = "Computer Players Module"
+__author__ = "CoolCat467"
+__version__ = "0.0.0"
+__license__ = "GNU General Public License Version 3"
+
+from azul_computer_players.minimax_ai import run
+
+if __name__ == "__main__":
+ run()
diff --git a/src/azul_computer_players/py.typed b/src/azul_computer_players/py.typed
new file mode 100644
index 0000000..e69de29
From ea88f1fde7474fdebd537e9ba74a61c72a43dfc0 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 6 Feb 2026 22:28:23 -0600
Subject: [PATCH 63/67] Rename minimax ai module
---
src/azul_computer_players/{MiniMax_AI.py => minimax_ai.py} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/azul_computer_players/{MiniMax_AI.py => minimax_ai.py} (100%)
diff --git a/src/azul_computer_players/MiniMax_AI.py b/src/azul_computer_players/minimax_ai.py
similarity index 100%
rename from src/azul_computer_players/MiniMax_AI.py
rename to src/azul_computer_players/minimax_ai.py
From 2e6985f2cd6cbc91d042964c50bcf31e7662615e Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 6 Feb 2026 22:31:10 -0600
Subject: [PATCH 64/67] Register cli commands for running server and minimax ai
---
pyproject.toml | 3 +
src/azul_computer_players/minimax_ai.py | 8 +-
test-requirements.txt | 108 ------------------------
3 files changed, 8 insertions(+), 111 deletions(-)
delete mode 100644 test-requirements.txt
diff --git a/pyproject.toml b/pyproject.toml
index 08f0e4d..39879b7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,6 +54,9 @@ version = {attr = "azul.game.__version__"}
[project.gui-scripts]
azul_game = "azul.game:cli_run"
+azul_game_server = "azul.server:cli_run"
+azul_game_minimax_ai_client = "azul_computer_players.minimax_ai:run"
+
[project.optional-dependencies]
tests = [
diff --git a/src/azul_computer_players/minimax_ai.py b/src/azul_computer_players/minimax_ai.py
index ce33d73..4adbd81 100755
--- a/src/azul_computer_players/minimax_ai.py
+++ b/src/azul_computer_players/minimax_ai.py
@@ -16,15 +16,17 @@
from math import inf as infinity
from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias, TypeVar
-from machine_client import RemoteState, run_clients_in_local_servers_sync
-from minimax import Minimax, MinimaxResult, Player
-
from azul.state import (
Phase,
SelectableDestinationTiles,
SelectableSourceTiles,
State,
)
+from azul_computer_players.machine_client import (
+ RemoteState,
+ run_clients_in_local_servers_sync,
+)
+from azul_computer_players.minimax import Minimax, MinimaxResult, Player
if TYPE_CHECKING:
from collections.abc import Iterable
diff --git a/test-requirements.txt b/test-requirements.txt
deleted file mode 100644
index 1ef3a8a..0000000
--- a/test-requirements.txt
+++ /dev/null
@@ -1,108 +0,0 @@
-# This file was autogenerated by uv via the following command:
-# uv pip compile --universal --python-version=3.10 test-requirements.in -o test-requirements.txt
-attrs==25.4.0
- # via
- # outcome
- # trio
-black==26.1.0 ; implementation_name == 'cpython'
- # via -r test-requirements.in
-cffi==2.0.0 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_python_implementation != 'PyPy'
- # via
- # cryptography
- # trio
-click==8.3.1 ; implementation_name == 'cpython'
- # via black
-codespell==2.4.1
- # via -r test-requirements.in
-colorama==0.4.6 ; sys_platform == 'win32'
- # via
- # click
- # pytest
-coverage==7.13.2
- # via
- # -r test-requirements.in
- # pytest-cov
-cryptography==46.0.4
- # via -r test-requirements.in
-exceptiongroup==1.3.1 ; python_full_version < '3.11'
- # via
- # -r test-requirements.in
- # pytest
- # trio
-idna==3.11
- # via trio
-iniconfig==2.3.0
- # via pytest
-librt==0.7.8 ; platform_python_implementation != 'PyPy'
- # via mypy
-mypy==1.19.1
- # via -r test-requirements.in
-mypy-extensions==1.1.0
- # via
- # -r test-requirements.in
- # black
- # mypy
-numpy==2.1.3
- # via -r test-requirements.in
-orjson==3.11.6 ; implementation_name == 'cpython'
- # via -r test-requirements.in
-outcome==1.3.0.post0
- # via
- # pytest-trio
- # trio
-packaging==26.0
- # via
- # black
- # pytest
-pathspec==1.0.4
- # via
- # black
- # mypy
-platformdirs==4.5.1 ; implementation_name == 'cpython'
- # via black
-pluggy==1.6.0
- # via
- # pytest
- # pytest-cov
-pycparser==3.0 ; (implementation_name != 'PyPy' and implementation_name != 'pypy' and os_name == 'nt') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy')
- # via cffi
-pygame==2.6.1
- # via -r test-requirements.in
-pygments==2.19.2
- # via pytest
-pytest==9.0.2
- # via
- # -r test-requirements.in
- # pytest-cov
- # pytest-trio
-pytest-cov==7.0.0
- # via -r test-requirements.in
-pytest-trio==0.8.0
- # via -r test-requirements.in
-pytokens==0.4.1 ; implementation_name == 'cpython'
- # via black
-ruff==0.14.14
- # via -r test-requirements.in
-sniffio==1.3.1
- # via trio
-sortedcontainers==2.4.0
- # via trio
-tomli==2.4.0 ; python_full_version <= '3.11'
- # via
- # black
- # coverage
- # mypy
- # pytest
-trio==0.27.0
- # via
- # -r test-requirements.in
- # pytest-trio
-typing-extensions==4.15.0
- # via
- # -r test-requirements.in
- # black
- # cryptography
- # exceptiongroup
- # mypy
-uv==0.9.28
- # via -r test-requirements.in
From 43a2f3c747cd58dd49539750e37af84d74656113 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 6 Feb 2026 23:04:02 -0600
Subject: [PATCH 65/67] Fix type issues
---
src/azul/client.py | 2 +-
src/azul/game.py | 5 +-
src/azul_computer_players/machine_client.py | 6 +-
src/azul_computer_players/minimax_ai.py | 133 ++++++++++----------
4 files changed, 73 insertions(+), 73 deletions(-)
diff --git a/src/azul/client.py b/src/azul/client.py
index e10e70e..7e9bf5f 100644
--- a/src/azul/client.py
+++ b/src/azul/client.py
@@ -258,7 +258,7 @@ async def raise_disconnect(self, message_key: str) -> None:
async def write_event(
self,
- event: Event[bytes | bytearray | memoryview],
+ event: Event[bytes | bytearray],
) -> None:
"""Send event to network if running, otherwise does nothing.
diff --git a/src/azul/game.py b/src/azul/game.py
index 5e1ae2e..a136401 100644
--- a/src/azul/game.py
+++ b/src/azul/game.py
@@ -866,9 +866,8 @@ async def handle_game_board_data(
assert array.ndim == 2
assert len(array.shape) == 2
- w, h = array.shape
- # Prove to typechecker that array is 2D
- self.data = array.reshape((w, h))
+ # w, h = array.shape
+ self.data = array
self.update_image()
self.visible = True
diff --git a/src/azul_computer_players/machine_client.py b/src/azul_computer_players/machine_client.py
index 88b0602..9a9e115 100644
--- a/src/azul_computer_players/machine_client.py
+++ b/src/azul_computer_players/machine_client.py
@@ -6,6 +6,7 @@
__author__ = "CoolCat467"
__version__ = "0.0.0"
+import traceback
from abc import ABCMeta, abstractmethod
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, TypeAlias, cast
@@ -501,4 +502,7 @@ def run_clients_in_local_servers_sync(
remote_state_class: type[RemoteState],
) -> None:
"""Run clients in local servers."""
- trio.run(run_clients_in_local_servers, remote_state_class)
+ try:
+ trio.run(run_clients_in_local_servers, remote_state_class)
+ except Exception as exc:
+ traceback.print_exception(exc)
diff --git a/src/azul_computer_players/minimax_ai.py b/src/azul_computer_players/minimax_ai.py
index 4adbd81..e36d767 100755
--- a/src/azul_computer_players/minimax_ai.py
+++ b/src/azul_computer_players/minimax_ai.py
@@ -12,9 +12,10 @@
__version__ = "0.0.0"
import time
+from collections.abc import Iterable
from enum import IntEnum, auto
from math import inf as infinity
-from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias, TypeVar
+from typing import Any, ClassVar, Self, TypeAlias, TypeVar
from azul.state import (
Phase,
@@ -28,12 +29,6 @@
)
from azul_computer_players.minimax import Minimax, MinimaxResult, Player
-if TYPE_CHECKING:
- from collections.abc import Iterable
- from typing import Self
-
- from mypy_extensions import u8
-
T = TypeVar("T")
Action: TypeAlias = (
tuple[SelectableDestinationTiles, ...]
@@ -66,7 +61,7 @@ def _factory_offer_maybe_next_turn(self) -> Self:
return new_state
-class MinimaxWithID(Minimax[AutoWallState, Action]):
+class MinimaxWithID(Minimax[State, Action]):
"""Minimax with ID."""
__slots__ = ()
@@ -211,8 +206,14 @@ def alphabeta_transposition_table(
# 2) Store in transposition_table
result = MinimaxResult(value, best_action)
- cls._transposition_table_store(state_h, depth, result, a, b)
- return result
+ cls._transposition_table_store(
+ state_h,
+ depth,
+ result, # type: ignore[arg-type]
+ a,
+ b,
+ )
+ return result # type: ignore[return-value]
@classmethod
def iterative_deepening(
@@ -259,36 +260,41 @@ def iterative_deepening(
return best_result
-# Minimax[tuple[AutoWallState, u8], Action]
+MAX_PLAYER = 0
+
+
+# Minimax[tuple[State, u8], Action]
class AzulMinimax(MinimaxWithID):
"""Minimax Algorithm for Azul."""
__slots__ = ()
- ## @classmethod
- ## def hash_state(cls, state: AutoWallState) -> int:
- ## """Return state hash value."""
- ## # For small games you might do: return hash(state)
- ## # For larger, use Zobrist or custom.
- ## return hash((state.size, tuple(state.pieces.items()), state.turn))
+ @classmethod
+ def hash_state(cls, state: AutoWallState) -> int: # type: ignore[override]
+ """Return state hash value."""
+ # For small games you might do: return hash(state)
+ # For larger, use Zobrist or custom.
+ ## return hash((state.size, tuple(state.pieces.items()), state.turn))
+ return hash(
+ tuple(tuple(x) if isinstance(x, Iterable) else x for x in state),
+ )
@staticmethod
- def value(state: tuple[AutoWallState, u8]) -> int | float:
+ def value(state: State) -> int | float:
"""Return value of given game state."""
# Real
- real_state, max_player = state
if AzulMinimax.terminal(state):
- winner, _score = real_state.get_win_order()[0]
- if winner == max_player:
+ winner, _score = state.get_win_order()[0]
+ if winner == MAX_PLAYER:
return 10
return -10
# Heuristic
min_ = 0
max_ = 0
- for player_id, player_data in real_state.player_data.items():
+ for player_id, player_data in state.player_data.items():
score = player_data.get_end_of_game_score()
score += player_data.get_floor_line_scoring()
- if player_id == max_player:
+ if player_id == MAX_PLAYER:
max_ += score
else:
min_ += score
@@ -298,64 +304,51 @@ def value(state: tuple[AutoWallState, u8]) -> int | float:
return (max_ - min_) / (abs(max_) + abs(min_) + 1)
@staticmethod
- def terminal(state: tuple[AutoWallState, u8]) -> bool:
+ def terminal(state: State) -> bool:
"""Return if game state is terminal."""
- real_state, _max_player = state
- return real_state.current_phase == Phase.end
+ return state.current_phase == Phase.end
@staticmethod
- def player(state: tuple[AutoWallState, u8]) -> Player:
+ def player(state: State) -> Player:
"""Return Player enum from current state's turn."""
- real_state, max_player = state
- return (
- Player.MAX if real_state.current_turn == max_player else Player.MIN
- )
+ return Player.MAX if state.current_turn == MAX_PLAYER else Player.MIN
@staticmethod
- def actions(state: tuple[AutoWallState, u8]) -> Iterable[Action]:
+ def actions(state: State) -> Iterable[Action]:
"""Return all actions that are able to be performed for the current player in the given state."""
- real_state, _max_player = state
- return tuple(real_state.yield_actions())
+ return tuple(state.yield_actions())
## print(f'{len(actions) = }')
@staticmethod
def result(
- state: tuple[AutoWallState, u8],
+ state: State,
action: Action,
- ) -> tuple[AutoWallState, u8]:
+ ) -> State:
"""Return new state after performing given action on given current state."""
- real_state, max_player = state
- return (real_state.preform_action(action), max_player)
-
- @classmethod
- def adaptive_depth_minimax(
- cls,
- state: tuple[AutoWallState, u8],
- ) -> MinimaxResult[Action]:
- """Adaptive depth minimax."""
- # TODO
- depth = 1
- return cls.alphabeta(state, depth)
-
- @classmethod
- def alphabeta(
- cls,
- state: tuple[AutoWallState, u8],
- depth: int | None = 5,
- a: int | float = -infinity,
- b: int | float = infinity,
- ) -> MinimaxResult[
- tuple[SelectableDestinationTiles, ...]
- | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]]
- ]:
- """Return minimax alphabeta pruning result best action for given current state."""
- new_state, player = state
- if (
- new_state.current_phase == Phase.wall_tiling
- and not new_state.variant_play
- ):
- new_state = new_state.apply_auto_wall_tiling()
- return super().alphabeta((new_state, player), depth, a, b)
+ ## real_state, MAX_PLAYER = state
+ ## return (real_state.preform_action(action), MAX_PLAYER)
+ return state.preform_action(action)
+
+
+## @classmethod
+## def alphabeta(
+## cls,
+## state: tuple[State, u8],
+## depth: int | None = 5,
+## a: int | float = -infinity,
+## b: int | float = infinity,
+## ) -> MinimaxResult[
+## tuple[SelectableDestinationTiles, ...]
+## | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]]
+## ]:
+## """Return minimax alphabeta pruning result best action for given current state."""
+## new_state, player = state
+## if (
+## new_state.current_phase == Phase.wall_tiling
+## and not new_state.variant_play
+## ):
+## new_state = new_state.apply_auto_wall_tiling()
+## return super().alphabeta((new_state, player), depth, a, b)
class MinimaxPlayer(RemoteState):
@@ -374,9 +367,13 @@ async def preform_turn(self) -> Action:
## self.state, 4, 5
##)
##value, action = CheckersMinimax.minimax(self.state, 4)
+ if not isinstance(self.state, AutoWallState):
+ self.state = AutoWallState._make(self.state)
assert isinstance(self.state, AutoWallState)
## value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 2)
## value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 4)
+ global MAX_PLAYER
+ MAX_PLAYER = self.playing_as
value, action = AzulMinimax.iterative_deepening(
self.state,
2,
From e8d860fe035ed061991fe2c322cdce909cc9233d Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Fri, 6 Feb 2026 23:55:58 -0600
Subject: [PATCH 66/67] Hack to make state hashable
---
src/azul_computer_players/minimax_ai.py | 26 ++++++++++++++++++++-----
1 file changed, 21 insertions(+), 5 deletions(-)
diff --git a/src/azul_computer_players/minimax_ai.py b/src/azul_computer_players/minimax_ai.py
index e36d767..f3171c9 100755
--- a/src/azul_computer_players/minimax_ai.py
+++ b/src/azul_computer_players/minimax_ai.py
@@ -12,7 +12,7 @@
__version__ = "0.0.0"
import time
-from collections.abc import Iterable
+from collections.abc import Hashable, Iterable, Mapping
from enum import IntEnum, auto
from math import inf as infinity
from typing import Any, ClassVar, Self, TypeAlias, TypeVar
@@ -263,6 +263,24 @@ def iterative_deepening(
MAX_PLAYER = 0
+def convert_hashable(obj: object) -> Hashable:
+ """Convert object to hashable object."""
+ exc: TypeError | None = None
+ try:
+ hash(obj)
+ except TypeError as exc: # noqa: F841
+ pass
+ else:
+ return obj
+ if isinstance(obj, Mapping):
+ return tuple(map(convert_hashable, obj.items()))
+ if isinstance(obj, Iterable):
+ return tuple(map(convert_hashable, obj))
+ if exc is not None:
+ raise NotImplementedError(type(obj)) from exc
+ raise NotImplementedError(type(obj))
+
+
# Minimax[tuple[State, u8], Action]
class AzulMinimax(MinimaxWithID):
"""Minimax Algorithm for Azul."""
@@ -275,9 +293,7 @@ def hash_state(cls, state: AutoWallState) -> int: # type: ignore[override]
# For small games you might do: return hash(state)
# For larger, use Zobrist or custom.
## return hash((state.size, tuple(state.pieces.items()), state.turn))
- return hash(
- tuple(tuple(x) if isinstance(x, Iterable) else x for x in state),
- )
+ return hash(convert_hashable(state))
@staticmethod
def value(state: State) -> int | float:
@@ -377,7 +393,7 @@ async def preform_turn(self) -> Action:
value, action = AzulMinimax.iterative_deepening(
self.state,
2,
- 6,
+ 20,
int(5 * 1e9),
)
if action is None:
From bbadf51e8a1786d43aca8fcc1fcefcffb5e1a1f3 Mon Sep 17 00:00:00 2001
From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com>
Date: Sat, 7 Feb 2026 01:18:19 -0600
Subject: [PATCH 67/67] Un-disable animation component
---
src/azul/sprite.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/azul/sprite.py b/src/azul/sprite.py
index 083af56..edab339 100644
--- a/src/azul/sprite.py
+++ b/src/azul/sprite.py
@@ -156,7 +156,7 @@ class ImageComponent(ComponentManager):
"""Allow sprite to use multiple images easily.
Components Supplied:
- # AnimationComponent
+ AnimationComponent
OutlineComponent
Requires Component:
@@ -182,7 +182,7 @@ def __init__(self) -> None:
self.add_components(
(
- # AnimationComponent(),
+ AnimationComponent(),
OutlineComponent(),
),
)