From 06e665443498d3234621f8bea6b383c0b5eb8ab3 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 19 Jan 2026 22:05:13 -0800 Subject: [PATCH 001/105] invalidate slices of model keys --- frontend/lib/cache.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/lib/cache.ts b/frontend/lib/cache.ts index abf8fe5c..df746d0e 100644 --- a/frontend/lib/cache.ts +++ b/frontend/lib/cache.ts @@ -22,14 +22,17 @@ localSocket.addCallback("connect", (connected: boolean) => { localSocket.addCallback("cache", (keys: CacheKey[]) => { for (const key of keys) { - void queryClient.invalidateQueries( - { - queryKey: key, - type: "active", - exact: true, - }, - { cancelRefetch: true }, - ); + for (let i = key.length; i > 0; --i) { + // Invalidate super-sets of the stale model + void queryClient.invalidateQueries( + { + queryKey: key.slice(0, i), + type: "active", + exact: true, + }, + { cancelRefetch: true }, + ); + } } }); From 9eecb79e8e39bf1c0c8854ba5132aaad54fd3214 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 21 Jan 2026 17:43:11 -0800 Subject: [PATCH 002/105] upgrade dependencies --- uv.lock | 166 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 84 insertions(+), 82 deletions(-) diff --git a/uv.lock b/uv.lock index a2c208e9..9dbbf3f7 100644 --- a/uv.lock +++ b/uv.lock @@ -197,7 +197,7 @@ standard = [ [[package]] name = "fastapi-cloud-cli" -version = "0.9.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastar" }, @@ -209,9 +209,9 @@ dependencies = [ { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/e5/95ba86183e9cf7357cbd1c101bb629fc6915750eae4b5b94205c127c31c8/fastapi_cloud_cli-0.9.0.tar.gz", hash = "sha256:07930591122ee4aefd113ea5355fca33141af31195da9038be526bacd5accbfe", size = 31614, upload-time = "2026-01-09T16:30:26.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/15/6c3d85d63964340fde6f36cc80f3f365d35f371e6a918d68ff3a3d588ef2/fastapi_cloud_cli-0.11.0.tar.gz", hash = "sha256:ecc83a5db106be35af528eccb01aa9bced1d29783efd48c8c1c831cf111eea99", size = 36170, upload-time = "2026-01-15T09:51:33.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/fd/65cdeb2916419eaf5e61428e63ceec7af5463a2239b1583119d85b38a792/fastapi_cloud_cli-0.9.0-py3-none-any.whl", hash = "sha256:21bf02163cebb5664f59613269eb18f74cc9ea2323d972f049c7fafa7abed0d1", size = 23065, upload-time = "2026-01-09T16:30:24.85Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/60f79270a3320780be7e2ae8a1740cb98a692920b569ba420b97bcc6e175/fastapi_cloud_cli-0.11.0-py3-none-any.whl", hash = "sha256:76857b0f09d918acfcb50ade34682ba3b2079ca0c43fda10215de301f185a7f8", size = 26884, upload-time = "2026-01-15T09:51:34.471Z" }, ] [[package]] @@ -450,11 +450,11 @@ dev = [ [[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, upload-time = "2025-04-19T11:48:59.673Z" } +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, upload-time = "2025-04-19T11:48:57.875Z" }, + { 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]] @@ -591,7 +591,7 @@ wheels = [ [[package]] name = "pyinstaller" -version = "6.17.0" +version = "6.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altgraph" }, @@ -602,32 +602,32 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/80/9e0dad9c69a7cfd4b5aaede8c6225d762bab7247a2a6b7651e1995522001/pyinstaller-6.17.0.tar.gz", hash = "sha256:be372bd911392b88277e510940ac32a5c2a6ce4b8d00a311c78fa443f4f27313", size = 4014147, upload-time = "2025-11-24T19:43:32.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/b8/0fe3359920b0a4e7008e0e93ff383003763e3eee3eb31a07c52868722960/pyinstaller-6.18.0.tar.gz", hash = "sha256:cdc507542783511cad4856fce582fdc37e9f29665ca596889c663c83ec8c6ec9", size = 4034976, upload-time = "2026-01-13T03:13:23.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/f5/37e419d84d5284ecab11ef8b61306a3b978fe6f0fd69a9541e16bfd72e65/pyinstaller-6.17.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4e446b8030c6e5a2f712e3f82011ecf6c7ead86008357b0d23a0ec4bcde31dac", size = 1031880, upload-time = "2025-11-24T19:42:30.862Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b6/2e184879ab9cf90a1d2867fdd34d507c4d246b3cc52ca05aad00bfc70ee7/pyinstaller-6.17.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa9fd87aaa28239c6f0d0210114029bd03f8cac316a90bab071a5092d7c85ad7", size = 731968, upload-time = "2025-11-24T19:42:35.421Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/f529de98f7e5cce7904c19b224990003fc2267eda2ee5fdd8452acb420a9/pyinstaller-6.17.0-py3-none-manylinux2014_i686.whl", hash = "sha256:060b122e43e7c0b23e759a4153be34bd70914135ab955bb18a67181e0dca85a2", size = 743217, upload-time = "2025-11-24T19:42:39.286Z" }, - { url = "https://files.pythonhosted.org/packages/a3/10/c02bfbb050cafc4c353cf69baf95407e211e1372bd286ab5ce5cbc13a30a/pyinstaller-6.17.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cd213d1a545c97dfe4a3c40e8213ff7c5127fc115c49229f27a3fa541503444b", size = 741119, upload-time = "2025-11-24T19:42:43.12Z" }, - { url = "https://files.pythonhosted.org/packages/11/9d/69fdacfd9335695f5900a376cfe3e4aed28f0720ffc15fee81fdb9d920bc/pyinstaller-6.17.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:89c0d18ba8b62c6607abd8cf2299ae5ffa5c36d8c47f39608ce8c3f357f6099f", size = 738111, upload-time = "2025-11-24T19:42:46.97Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1e/e8e36e1568f6865ac706c6e1f875c1a346ddaa9f9a8f923d66545d2240ed/pyinstaller-6.17.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2a147b83cdebb07855bd5a663600891550062373a2ca375c58eacead33741a27", size = 737795, upload-time = "2025-11-24T19:42:50.675Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/9dc0f81ccb746c27bfa6ee53164422fe47ee079c7a717d9c4791aba78797/pyinstaller-6.17.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:f8cfbbfa6708e54fb936df6dd6eafaf133e84efb0d2fe25b91cfeefa793c4ca4", size = 736891, upload-time = "2025-11-24T19:42:54.458Z" }, - { url = "https://files.pythonhosted.org/packages/97/e6/bed54821c1ebe1275c559661d3e7bfa23c406673b515252dfbf89db56c65/pyinstaller-6.17.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:97f4c1942f7b4cd73f9e38b49cc8f5f8a6fbb44922cb60dd3073a189b77ee1ae", size = 736752, upload-time = "2025-11-24T19:42:58.144Z" }, - { url = "https://files.pythonhosted.org/packages/c7/84/897d759198676b910d69d42640b6d25d50b449f2209e18127a974cf59dbe/pyinstaller-6.17.0-py3-none-win32.whl", hash = "sha256:ce0be227a037fd4be672226db709088565484f597d6b230bceec19850fdd4c85", size = 1317851, upload-time = "2025-11-24T19:43:04.361Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f5/6a122efe024433ecc34aab6f499e0bd2bbe059c639b77b0045aa2421b0bf/pyinstaller-6.17.0-py3-none-win_amd64.whl", hash = "sha256:b019940dbf7a01489d6b26f9fb97db74b504e0a757010f7ad078675befc85a82", size = 1378685, upload-time = "2025-11-24T19:43:10.395Z" }, - { url = "https://files.pythonhosted.org/packages/c4/96/14991773c9e599707a53594429ccf372f9ee638df3b7d26b65fd1a7433f0/pyinstaller-6.17.0-py3-none-win_arm64.whl", hash = "sha256:3c92a335e338170df7e615f75279cfeea97ade89e6dd7694943c8c185460f7b7", size = 1320032, upload-time = "2025-11-24T19:43:16.388Z" }, + { url = "https://files.pythonhosted.org/packages/73/e6/51b0146a1a3eec619e58f5d69fb4e3d0f65a31cbddbeef557c9bb83eeed9/pyinstaller-6.18.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:cb7aa5a71bfa7c0af17a4a4e21855663c89e4bd7c40f1d337c8370636d8847c3", size = 1040056, upload-time = "2026-01-13T03:12:15.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9c/a3634c0ec8e1ed31b373b548848b5c0b39b56edc191cf737e697d484ec23/pyinstaller-6.18.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:07785459b3bf8a48889eac0b4d0667ade84aef8930ce030bc7cbb32f41283b33", size = 734971, upload-time = "2026-01-13T03:12:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/2c/04/6756442078ccfcd552ccce636be1574035e62f827ffa1f5d8a0382682546/pyinstaller-6.18.0-py3-none-manylinux2014_i686.whl", hash = "sha256:f998675b7ccb2dabbb1dc2d6f18af61d55428ad6d38e6c4d700417411b697d37", size = 746637, upload-time = "2026-01-13T03:12:29.302Z" }, + { url = "https://files.pythonhosted.org/packages/54/39/fbc56519000cdbf450f472692a7b9b55d42077ce8529f1be631db7b75a36/pyinstaller-6.18.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:779817a0cf69604cddcdb5be1fd4959dc2ce048d6355c73e5da97884df2f3387", size = 744343, upload-time = "2026-01-13T03:12:33.369Z" }, + { url = "https://files.pythonhosted.org/packages/36/f2/50887badf282fee776e83d1e4feab74c026f50a1ea16e109ed939e32aa28/pyinstaller-6.18.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:31b5d109f8405be0b7cddcede43e7b074792bc9a5bbd54ec000a3e779183c2af", size = 741084, upload-time = "2026-01-13T03:12:37.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/08/3a1419183e4713ef77d912ecbdd6ef858689ed9deb34d547133f724ca745/pyinstaller-6.18.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4328c9837f1aef4fe1a127d4ff1b09a12ce53c827ce87c94117628b0e1fd098b", size = 740943, upload-time = "2026-01-13T03:12:41.589Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/309305e36d116f1434b42d91c420ff951fa79b2c398bbd59930c830450be/pyinstaller-6.18.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:3638fc81eb948e5e5eab1d4ad8f216e3fec6d4a350648304f0adb227b746ee5e", size = 740107, upload-time = "2026-01-13T03:12:45.694Z" }, + { url = "https://files.pythonhosted.org/packages/83/0f/a59a95cd1df59ddbc9e74d5a663387551333bcf19a5dd3086f5c81a2e83c/pyinstaller-6.18.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe59da34269e637f97fd3c43024f764586fc319141d245ff1a2e9af1036aa3", size = 739843, upload-time = "2026-01-13T03:12:49.728Z" }, + { url = "https://files.pythonhosted.org/packages/9a/09/e7a870e7205cdbd2f8785010a5d3fe48a9df2591156ee34a8b29b774fa14/pyinstaller-6.18.0-py3-none-win32.whl", hash = "sha256:496205e4fa92ec944f9696eb597962a83aef4d4c3479abfab83d730e1edf016b", size = 1323811, upload-time = "2026-01-13T03:12:55.717Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d5/48eef2002b6d3937ceac2717fe17e9ca3a43a4c9826bafee367dfc75ba85/pyinstaller-6.18.0-py3-none-win_amd64.whl", hash = "sha256:976fabd90ecfbda47571c87055ad73413ec615ff7dea35e12a4304174de78de9", size = 1384389, upload-time = "2026-01-13T03:13:01.993Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/1a88e6e94107de3ea1c842fd59c3aa132d344ad8e52ea458ffa9a748726e/pyinstaller-6.18.0-py3-none-win_arm64.whl", hash = "sha256:dba4b70e3c9ba09aab51152c72a08e58a751851548f77ad35944d32a300c8381", size = 1324869, upload-time = "2026-01-13T03:13:08.192Z" }, ] [[package]] name = "pyinstaller-hooks-contrib" -version = "2025.11" +version = "2026.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/2f/2c68b6722d233dae3e5243751aafc932940b836919cfaca22dd0c60d417c/pyinstaller_hooks_contrib-2025.11.tar.gz", hash = "sha256:dfe18632e06655fa88d218e0d768fd753e1886465c12a6d4bce04f1aaeec917d", size = 169183, upload-time = "2025-12-23T12:59:37.361Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/8f/8052ff65067697ee80fde45b9731842e160751c41ac5690ba232c22030e8/pyinstaller_hooks_contrib-2026.0.tar.gz", hash = "sha256:0120893de491a000845470ca9c0b39284731ac6bace26f6849dea9627aaed48e", size = 170311, upload-time = "2026-01-20T00:15:23.922Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c4/3a096c6e701832443b957b9dac18a163103360d0c7f5842ca41695371148/pyinstaller_hooks_contrib-2025.11-py3-none-any.whl", hash = "sha256:777e163e2942474aa41a8e6d31ac1635292d63422c3646c176d584d04d971c34", size = 449478, upload-time = "2025-12-23T12:59:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b1/9da6ec3e88696018ee7bb9dc4a7310c2cfaebf32923a19598cd342767c10/pyinstaller_hooks_contrib-2026.0-py3-none-any.whl", hash = "sha256:0590db8edeba3e6c30c8474937021f5cd39c0602b4d10f74a064c73911efaca5", size = 452318, upload-time = "2026-01-20T00:15:21.88Z" }, ] [[package]] @@ -806,28 +806,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, - { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, - { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, - { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, - { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, - { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, - { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, - { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, - { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, - { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, - { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, - { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, ] [[package]] @@ -841,24 +841,24 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.49.0" +version = "2.50.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/94/23ac26616a883f492428d9ee9ad6eee391612125326b784dbfc30e1e7bab/sentry_sdk-2.49.0.tar.gz", hash = "sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30", size = 387228, upload-time = "2026-01-08T09:56:25.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/8a/3c4f53d32c21012e9870913544e56bfa9e931aede080779a0f177513f534/sentry_sdk-2.50.0.tar.gz", hash = "sha256:873437a989ee1b8b25579847bae8384515bf18cfed231b06c591b735c1781fe3", size = 401233, upload-time = "2026-01-20T12:53:16.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/43/1c586f9f413765201234541857cb82fda076f4b0f7bad4a0ec248da39cf3/sentry_sdk-2.49.0-py2.py3-none-any.whl", hash = "sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0", size = 415693, upload-time = "2026-01-08T09:56:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5b/cbc2bb9569f03c8e15d928357e7e6179e5cfab45544a3bbac8aec4caf9be/sentry_sdk-2.50.0-py2.py3-none-any.whl", hash = "sha256:0ef0ed7168657ceb5a0be081f4102d92042a125462d1d1a29277992e344e749e", size = 424961, upload-time = "2026-01-20T12:53:14.826Z" }, ] [[package]] name = "setuptools" -version = "80.9.0" +version = "80.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/ff/f75651350db3cf2ef767371307eb163f3cc1ac03e16fdf3ac347607f7edb/setuptools-80.10.1.tar.gz", hash = "sha256:bf2e513eb8144c3298a3bd28ab1a5edb739131ec5c22e045ff93cd7f5319703a", size = 1229650, upload-time = "2026-01-21T09:42:03.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/f963c61683a39084aa575f98089253e1e852a4417cb8a3a8a422923a5246/setuptools-80.10.1-py3-none-any.whl", hash = "sha256:fc30c51cbcb8199a219c12cc9c281b5925a4978d212f84229c909636d9f6984e", size = 1099859, upload-time = "2026-01-21T09:42:00.688Z" }, ] [[package]] @@ -884,23 +884,26 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.45" +version = "2.0.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, ] [package.optional-dependencies] @@ -922,27 +925,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/45/5ae578480168d4b3c08cf8e5eac3caf8eb7acdb1a06a9bed7519564bd9b4/ty-0.0.11.tar.gz", hash = "sha256:ebcbc7d646847cb6610de1da4ffc849d8b800e29fd1e9ebb81ba8f3fbac88c25", size = 4920340, upload-time = "2026-01-09T21:06:01.592Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/34/b1d05cdcd01589a8d2e63011e0a1e24dcefdc2a09d024fee3e27755963f6/ty-0.0.11-py3-none-linux_armv6l.whl", hash = "sha256:68f0b8d07b0a2ea7ec63a08ba2624f853e4f9fa1a06fce47fb453fa279dead5a", size = 9521748, upload-time = "2026-01-09T21:06:13.221Z" }, - { url = "https://files.pythonhosted.org/packages/43/21/f52d93f4b3784b91bfbcabd01b84dc82128f3a9de178536bcf82968f3367/ty-0.0.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbf82d7ef0618e9ae3cc3c37c33abcfa302c9b3e3b8ff11d71076f98481cb1a8", size = 9454903, upload-time = "2026-01-09T21:06:42.363Z" }, - { url = "https://files.pythonhosted.org/packages/ad/01/3a563dba8b1255e474c35e1c3810b7589e81ae8c41df401b6a37c8e2cde9/ty-0.0.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:121987c906e02264c3b511b95cb9f8a3cdd66f3283b8bbab678ca3525652e304", size = 8823417, upload-time = "2026-01-09T21:06:26.315Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b1/99b87222c05d3a28fb7bbfb85df4efdde8cb6764a24c1b138f3a615283dd/ty-0.0.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:999390b6cc045fe5e1b3da1c2c9ae8e8c0def23b69455e7c9191ba9ffd747023", size = 9290785, upload-time = "2026-01-09T21:05:59.028Z" }, - { url = "https://files.pythonhosted.org/packages/3d/9f/598809a8fff2194f907ba6de07ac3d7b7788342592d8f8b98b1b50c2fb49/ty-0.0.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed504d78eb613c49be3c848f236b345b6c13dc6bcfc4b202790a60a97e1d8f35", size = 9359392, upload-time = "2026-01-09T21:06:37.459Z" }, - { url = "https://files.pythonhosted.org/packages/71/3e/aeea2a97b38f3dcd9f8224bf83609848efa4bc2f484085508165567daa7b/ty-0.0.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fedc8b43cc8a9991e0034dd205f957a8380dd29bfce36f2a35b5d321636dfd9", size = 9852973, upload-time = "2026-01-09T21:06:21.245Z" }, - { url = "https://files.pythonhosted.org/packages/72/40/86173116995e38f954811a86339ac4c00a2d8058cc245d3e4903bc4a132c/ty-0.0.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0808bdfb7efe09881bf70249b85b0498fb8b75fbb036ce251c496c20adb10075", size = 10796113, upload-time = "2026-01-09T21:06:16.034Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/97c92c401dacae9baa3696163ebe8371635ebf34ba9fda781110d0124857/ty-0.0.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07185b3e38b18c562056dfbc35fb51d866f872977ea1ebcd64ca24a001b5b4f1", size = 10432137, upload-time = "2026-01-09T21:06:07.498Z" }, - { url = "https://files.pythonhosted.org/packages/18/10/9ab43f3cfc5f7792f6bc97620f54d0a0a81ef700be84ea7f6be330936a99/ty-0.0.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5c72f1ada8eb5be984502a600f71d1a3099e12fb6f3c0607aaba2f86f0e9d80", size = 10240520, upload-time = "2026-01-09T21:06:34.823Z" }, - { url = "https://files.pythonhosted.org/packages/74/18/8dd4fe6df1fd66f3e83b4798eddb1d8482d9d9b105f25099b76703402ebb/ty-0.0.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25f88e8789072830348cb59b761d5ced70642ed5600673b4bf6a849af71eca8b", size = 9973340, upload-time = "2026-01-09T21:06:39.657Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0b/fb2301450cf8f2d7164944d6e1e659cac9ec7021556cc173d54947cf8ef4/ty-0.0.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f370e1047a62dcedcd06e2b27e1f0b16c7f8ea2361d9070fcbf0d0d69baaa192", size = 9262101, upload-time = "2026-01-09T21:06:28.989Z" }, - { url = "https://files.pythonhosted.org/packages/f7/8c/d6374af023541072dee1c8bcfe8242669363a670b7619e6fffcc7415a995/ty-0.0.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:52be34047ed6177bfcef9247459a767ec03d775714855e262bca1fb015895e8a", size = 9382756, upload-time = "2026-01-09T21:06:24.097Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/edd1e63ffa8d49d720c475c2c1c779084e5efe50493afdc261938705d10a/ty-0.0.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9e5762ccb3778779378020b8d78f936b3f52ea83f18785319cceba3ae85d8e6", size = 9553944, upload-time = "2026-01-09T21:06:18.426Z" }, - { url = "https://files.pythonhosted.org/packages/35/cd/4afdb0d182d23d07ff287740c4954cc6dde5c3aed150ec3f2a1d72b00f71/ty-0.0.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9334646ee3095e778e3dbc45fdb2bddfc16acc7804283830ad84991ece16dd7", size = 10060365, upload-time = "2026-01-09T21:06:45.083Z" }, - { url = "https://files.pythonhosted.org/packages/d1/94/a009ad9d8b359933cfea8721c689c0331189be28650d74dcc6add4d5bb09/ty-0.0.11-py3-none-win32.whl", hash = "sha256:44cfb7bb2d6784bd7ffe7b5d9ea90851d9c4723729c50b5f0732d4b9a2013cfc", size = 9040448, upload-time = "2026-01-09T21:06:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/df/04/5a5dfd0aec0ea99ead1e824ee6e347fb623c464da7886aa1e3660fb0f36c/ty-0.0.11-py3-none-win_amd64.whl", hash = "sha256:1bb205db92715d4a13343bfd5b0c59ce8c0ca0daa34fb220ec9120fc66ccbda7", size = 9780112, upload-time = "2026-01-09T21:06:04.69Z" }, - { url = "https://files.pythonhosted.org/packages/ad/07/47d4fccd7bcf5eea1c634d518d6cb233f535a85d0b63fcd66815759e2fa0/ty-0.0.11-py3-none-win_arm64.whl", hash = "sha256:4688bd87b2dc5c85da277bda78daba14af2e66f3dda4d98f3604e3de75519eba", size = 9194038, upload-time = "2026-01-09T21:06:10.152Z" }, +version = "0.0.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/dc/b607f00916f5a7c52860b84a66dc17bc6988e8445e96b1d6e175a3837397/ty-0.0.13.tar.gz", hash = "sha256:7a1d135a400ca076407ea30012d1f75419634160ed3b9cad96607bf2956b23b3", size = 4999183, upload-time = "2026-01-21T13:21:16.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/df/3632f1918f4c0a33184f107efc5d436ab6da147fd3d3b94b3af6461efbf4/ty-0.0.13-py3-none-linux_armv6l.whl", hash = "sha256:1b2b8e02697c3a94c722957d712a0615bcc317c9b9497be116ef746615d892f2", size = 9993501, upload-time = "2026-01-21T13:21:26.628Z" }, + { url = "https://files.pythonhosted.org/packages/92/87/6a473ced5ac280c6ce5b1627c71a8a695c64481b99aabc798718376a441e/ty-0.0.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f15cdb8e233e2b5adfce673bb21f4c5e8eaf3334842f7eea3c70ac6fda8c1de5", size = 9860986, upload-time = "2026-01-21T13:21:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9b/d89ae375cf0a7cd9360e1164ce017f8c753759be63b6a11ed4c944abe8c6/ty-0.0.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0819e89ac9f0d8af7a062837ce197f0461fee2fc14fd07e2c368780d3a397b73", size = 9350748, upload-time = "2026-01-21T13:21:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a6/9ad58518056fab344b20c0bb2c1911936ebe195318e8acc3bc45ac1c6b6b/ty-0.0.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de79f481084b7cc7a202ba0d7a75e10970d10ffa4f025b23f2e6b7324b74886", size = 9849884, upload-time = "2026-01-21T13:21:21.886Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c3/8add69095fa179f523d9e9afcc15a00818af0a37f2b237a9b59bc0046c34/ty-0.0.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4fb2154cff7c6e95d46bfaba283c60642616f20d73e5f96d0c89c269f3e1bcec", size = 9822975, upload-time = "2026-01-21T13:21:14.292Z" }, + { url = "https://files.pythonhosted.org/packages/a4/05/4c0927c68a0a6d43fb02f3f0b6c19c64e3461dc8ed6c404dde0efb8058f7/ty-0.0.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00be58d89337c27968a20d58ca553458608c5b634170e2bec82824c2e4cf4d96", size = 10294045, upload-time = "2026-01-21T13:21:30.505Z" }, + { url = "https://files.pythonhosted.org/packages/b4/86/6dc190838aba967557fe0bfd494c595d00b5081315a98aaf60c0e632aaeb/ty-0.0.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72435eade1fa58c6218abb4340f43a6c3ff856ae2dc5722a247d3a6dd32e9737", size = 10916460, upload-time = "2026-01-21T13:21:07.788Z" }, + { url = "https://files.pythonhosted.org/packages/04/40/9ead96b7c122e1109dfcd11671184c3506996bf6a649306ec427e81d9544/ty-0.0.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77a548742ee8f621d718159e7027c3b555051d096a49bb580249a6c5fc86c271", size = 10597154, upload-time = "2026-01-21T13:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7d/e832a2c081d2be845dc6972d0c7998914d168ccbc0b9c86794419ab7376e/ty-0.0.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da067c57c289b7cf914669704b552b6207c2cc7f50da4118c3e12388642e6b3f", size = 10410710, upload-time = "2026-01-21T13:21:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/31/e3/898be3a96237a32f05c4c29b43594dc3b46e0eedfe8243058e46153b324f/ty-0.0.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d1b50a01fffa140417fca5a24b658fbe0734074a095d5b6f0552484724474343", size = 9826299, upload-time = "2026-01-21T13:21:00.845Z" }, + { url = "https://files.pythonhosted.org/packages/bb/eb/db2d852ce0ed742505ff18ee10d7d252f3acfd6fc60eca7e9c7a0288a6d8/ty-0.0.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f33c46f52e5e9378378eca0d8059f026f3c8073ace02f7f2e8d079ddfe5207e", size = 9831610, upload-time = "2026-01-21T13:21:05.842Z" }, + { url = "https://files.pythonhosted.org/packages/9e/61/149f59c8abaddcbcbb0bd13b89c7741ae1c637823c5cf92ed2c644fcadef/ty-0.0.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:168eda24d9a0b202cf3758c2962cc295878842042b7eca9ed2965259f59ce9f2", size = 9978885, upload-time = "2026-01-21T13:21:10.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/026d4e4af60a80918a8d73d2c42b8262dd43ab2fa7b28d9743004cb88d57/ty-0.0.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4917678b95dc8cb399cc459fab568ba8d5f0f33b7a94bf840d9733043c43f29", size = 10506453, upload-time = "2026-01-21T13:20:56.633Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/8932833a4eca2df49c997a29afb26721612de8078ae79074c8fe87e17516/ty-0.0.13-py3-none-win32.whl", hash = "sha256:c1f2ec40daa405508b053e5b8e440fbae5fdb85c69c9ab0ee078f8bc00eeec3d", size = 9433482, upload-time = "2026-01-21T13:20:58.717Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fd/e8d972d1a69df25c2cecb20ea50e49ad5f27a06f55f1f5f399a563e71645/ty-0.0.13-py3-none-win_amd64.whl", hash = "sha256:8b7b1ab9f187affbceff89d51076038363b14113be29bda2ddfa17116de1d476", size = 10319156, upload-time = "2026-01-21T13:21:03.266Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c2/05fdd64ac003a560d4fbd1faa7d9a31d75df8f901675e5bed1ee2ceeff87/ty-0.0.13-py3-none-win_arm64.whl", hash = "sha256:1c9630333497c77bb9bcabba42971b96ee1f36c601dd3dcac66b4134f9fa38f0", size = 9808316, upload-time = "2026-01-21T13:20:54.053Z" }, ] [[package]] From e2da4184913076a00a589a4641bdda2a5959fb07 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 21 Jan 2026 17:45:56 -0800 Subject: [PATCH 003/105] make db id column a private python variable --- backend/src/core/database.py | 2 +- backend/src/game/bouts/dependencies.py | 2 +- backend/src/game/bouts/models.py | 4 ++-- backend/src/game/jams/dependencies.py | 2 +- backend/src/game/jams/models.py | 6 +++--- backend/src/game/rosters/dependencies.py | 2 +- backend/src/game/rosters/models.py | 2 +- backend/src/game/rulesets/wftda_2025.py | 4 ++-- backend/src/game/series/models.py | 2 +- backend/src/game/team_jams/models.py | 4 ++-- backend/src/game/teams/dependencies.py | 4 ++-- backend/src/game/teams/models.py | 4 ++-- backend/src/game/timeouts/dependencies.py | 2 +- backend/src/game/timeouts/models.py | 6 +++--- backend/src/game/utils.py | 2 +- backend/src/ws/service.py | 2 +- 16 files changed, 25 insertions(+), 25 deletions(-) diff --git a/backend/src/core/database.py b/backend/src/core/database.py index d9ae24e3..abd7d011 100644 --- a/backend/src/core/database.py +++ b/backend/src/core/database.py @@ -45,7 +45,7 @@ class BaseSQLModel(AsyncAttrs, DeclarativeBase): """ - id: Mapped[int | None] = mapped_column(nullable=False, primary_key=True) + _id: Mapped[int | None] = mapped_column(nullable=False, primary_key=True) __abstract__: bool = True __type_annotation_map__: dict = {timedelta: _TimedeltaAsMilliseconds} diff --git a/backend/src/game/bouts/dependencies.py b/backend/src/game/bouts/dependencies.py index 432a3471..d8eab8ba 100644 --- a/backend/src/game/bouts/dependencies.py +++ b/backend/src/game/bouts/dependencies.py @@ -22,7 +22,7 @@ async def _get_bout( bout_id: Annotated[int, Query(alias='boutId')], ) -> BaseBout: # Query the database for the desired Bout - statement: Select[tuple[BaseBout]] = select(BaseBout).where(BaseBout.id == bout_id) + statement: Select[tuple[BaseBout]] = select(BaseBout).where(BaseBout._id == bout_id) results: Result[tuple[BaseBout]] = await session.execute(statement) try: diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 5f37bf41..10d57c8d 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -82,7 +82,7 @@ def __str__(self) -> str: str: a str representation of this Bout. """ - return f'[Bout ID: {self.id}]' + return f'[Bout ID: {self._id}]' def __init__(self, ruleset_name: str, *teams: BaseTeam) -> None: """Instantiate a Bout. @@ -97,7 +97,7 @@ def __init__(self, ruleset_name: str, *teams: BaseTeam) -> None: @override def cache_key(self) -> CacheKey: - return (self.__tablename__, self.id) + return (self.__tablename__, self._id) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/jams/dependencies.py b/backend/src/game/jams/dependencies.py index 35c22e62..f8c267ca 100644 --- a/backend/src/game/jams/dependencies.py +++ b/backend/src/game/jams/dependencies.py @@ -22,7 +22,7 @@ async def _get_jam( session: AsyncSessionDepends, jam_id: Annotated[int, Query(alias='jamId')], ) -> BaseJam: - statement: Select[tuple[BaseJam]] = select(BaseJam).where(BaseJam.id == jam_id) + statement: Select[tuple[BaseJam]] = select(BaseJam).where(BaseJam._id == jam_id) results: Result[tuple[BaseJam]] = await session.execute(statement) try: diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index 6dddffed..46f35881 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -47,7 +47,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): ) _ruleset: MappedSQLExpression[str] = column_property( - select(BaseBout.ruleset_name).where(BaseBout.id == bout_id).scalar_subquery() + select(BaseBout.ruleset_name).where(BaseBout._id == bout_id).scalar_subquery() ) __tablename__: str = 'jams' @@ -112,9 +112,9 @@ def get_team_jam(self, team: BaseTeam | int) -> TeamJam: """ if not isinstance(team, int): - if team.id is None: + if team._id is None: raise KeyError('this team does not exist') - team = team.id + team = team._id # Get the first TeamJam that has the specified Team ID team_jam: TeamJam | None = next( diff --git a/backend/src/game/rosters/dependencies.py b/backend/src/game/rosters/dependencies.py index a59f0d92..bcd7c0ab 100644 --- a/backend/src/game/rosters/dependencies.py +++ b/backend/src/game/rosters/dependencies.py @@ -16,7 +16,7 @@ async def _get_roster( roster_id: Annotated[int, Query(alias='rosterId')], ) -> Roster: results: Result[tuple[Roster]] = await session.execute( - select(Roster).where(Roster.id == roster_id) + select(Roster).where(Roster._id == roster_id) ) try: diff --git a/backend/src/game/rosters/models.py b/backend/src/game/rosters/models.py index c32ec8c7..d4b70efc 100644 --- a/backend/src/game/rosters/models.py +++ b/backend/src/game/rosters/models.py @@ -95,7 +95,7 @@ def __init__(self, name: str, league: str, mnemonic: str = '') -> None: @override def cache_key(self) -> CacheKey: - return (self.__tablename__, self.id) + return (self.__tablename__, self._id) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index 79a36aa3..81f9cc1a 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -348,8 +348,8 @@ def set_team(self, team: BaseTeam | None) -> None: raise GameRulesError('Official reviews can only be called by teams') logging.info( - f'Setting Timeout ID {self.id} calling team to ' - f'{f"Team ID {team.id}" if team is not None else "officials"} in Bout ID ' + f'Setting Timeout ID {self._id} calling team to ' + f'{f"Team ID {team._id}" if team is not None else "officials"} in Bout ID ' f'{self.bout_id}' ) diff --git a/backend/src/game/series/models.py b/backend/src/game/series/models.py index e8dc8b18..dc4778ea 100644 --- a/backend/src/game/series/models.py +++ b/backend/src/game/series/models.py @@ -35,7 +35,7 @@ class Series(CacheableSQLModel): @override def cache_key(self) -> CacheKey: # Special case where updating one Series invalidates the cache for all Series - return (self.__tablename__, self.id) + return (self.__tablename__, self._id) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/team_jams/models.py b/backend/src/game/team_jams/models.py index fa7fc618..6e1d85e0 100644 --- a/backend/src/game/team_jams/models.py +++ b/backend/src/game/team_jams/models.py @@ -53,10 +53,10 @@ class TeamJam(BaseSQLModel): ) jam_num: MappedSQLExpression[int] = column_property( - select(BaseJam.num).where(BaseJam.id == jam_id).scalar_subquery() + select(BaseJam.num).where(BaseJam._id == jam_id).scalar_subquery() ) period_num: MappedSQLExpression[int] = column_property( - select(BaseJam.period).where(BaseJam.id == jam_id).scalar_subquery() + select(BaseJam.period).where(BaseJam._id == jam_id).scalar_subquery() ) __tablename__: str = 'team_jams' diff --git a/backend/src/game/teams/dependencies.py b/backend/src/game/teams/dependencies.py index 3c49563d..8ef5b51e 100644 --- a/backend/src/game/teams/dependencies.py +++ b/backend/src/game/teams/dependencies.py @@ -23,7 +23,7 @@ async def _query_team_or_none( return None # Query the database for the desired Bout - statement: Select[tuple[BaseTeam]] = select(BaseTeam).where(BaseTeam.id == team_id) + statement: Select[tuple[BaseTeam]] = select(BaseTeam).where(BaseTeam._id == team_id) results: Result[tuple[BaseTeam]] = await session.execute(statement) team: BaseTeam = results.scalar_one() @@ -38,7 +38,7 @@ async def _get_team_or_none( return None # Query the database for the desired Bout - statement: Select[tuple[BaseTeam]] = select(BaseTeam).where(BaseTeam.id == team_id) + statement: Select[tuple[BaseTeam]] = select(BaseTeam).where(BaseTeam._id == team_id) results: Result[tuple[BaseTeam]] = await session.execute(statement) team: BaseTeam = results.scalar_one() diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index 63c5edda..02c7bb2d 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -66,14 +66,14 @@ class BaseTeam(BaseSQLModel): # Used to calculate the current Jam score _active_jam_id: MappedSQLExpression[int | None] = column_property( - select(BaseJam.id) + select(BaseJam._id) .where(BaseJam.start_timestamp != None) # noqa: E711 .order_by(desc(BaseJam.period), desc(BaseJam.num)) .scalar_subquery() ) _ruleset: MappedSQLExpression[str] = column_property( - select(BaseBout.ruleset_name).where(BaseBout.id == bout_id).scalar_subquery() + select(BaseBout.ruleset_name).where(BaseBout._id == bout_id).scalar_subquery() ) __tablename__: str = 'teams' diff --git a/backend/src/game/timeouts/dependencies.py b/backend/src/game/timeouts/dependencies.py index c3cd80d4..898ee25f 100644 --- a/backend/src/game/timeouts/dependencies.py +++ b/backend/src/game/timeouts/dependencies.py @@ -22,7 +22,7 @@ async def _get_timeout( timeout_id: Annotated[int, Query(alias='timeoutId')], ) -> BaseTimeout: statement: Select[tuple[BaseTimeout]] = select(BaseTimeout).where( - BaseTimeout.id == timeout_id + BaseTimeout._id == timeout_id ) results: Result[tuple[BaseTimeout]] = await session.execute(statement) diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index e61be35c..864d4d02 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -59,7 +59,7 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): ) ruleset: MappedSQLExpression[str] = column_property( - select(BaseBout.ruleset_name).where(BaseBout.id == bout_id).scalar_subquery() + select(BaseBout.ruleset_name).where(BaseBout._id == bout_id).scalar_subquery() ) __tablename__: str = 'timeouts' @@ -89,11 +89,11 @@ def __init__(self, bout: BaseBout, num: int) -> None: num (int): the unique Timeout number associated with this Bout. """ - super().__init__(_bout=bout, bout_id=bout.id, num=num) + super().__init__(_bout=bout, bout_id=bout._id, num=num) @override def cache_key(self) -> CacheKey: - return (self.__tablename__, self.bout_id, self.id) + return (self.__tablename__, self.bout_id, self._id) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/utils.py b/backend/src/game/utils.py index f6dcd7a7..4c8dff19 100644 --- a/backend/src/game/utils.py +++ b/backend/src/game/utils.py @@ -33,7 +33,7 @@ async def restore(self) -> Memento: # Query and detach the current state of the database object table: type[CacheableSQLModel] = self._detached_state_to_restore.__class__ statement: Select[tuple[CacheableSQLModel]] = select(table).where( - table.id == self._detached_state_to_restore.id + table._id == self._detached_state_to_restore._id ) results: Result[tuple[CacheableSQLModel]] = await session.execute(statement) current_state: CacheableSQLModel = results.scalar_one() diff --git a/backend/src/ws/service.py b/backend/src/ws/service.py index be63abab..0d00df23 100644 --- a/backend/src/ws/service.py +++ b/backend/src/ws/service.py @@ -114,7 +114,7 @@ async def invalidate_queries(models: Iterable[BaseSQLModel]) -> None: cache_keys: list[CacheKey] = [ cacheable.cache_key() for cacheable in cacheables - if cacheable.id is not None + if cacheable._id is not None ] if len(cache_keys) == 0: return From 1613f1f6bd1a6545a05b073f3657da2b59981dc2 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 21 Jan 2026 18:45:38 -0800 Subject: [PATCH 004/105] make database keys private python variables --- backend/src/game/bouts/models.py | 4 ++-- backend/src/game/bouts/schemas.py | 23 +++++++++---------- backend/src/game/clocks/schemas.py | 1 - backend/src/game/jams/models.py | 16 +++++++------ backend/src/game/jams/schemas.py | 3 +-- backend/src/game/rosters/models.py | 4 ++-- backend/src/game/rosters/schemas.py | 2 +- backend/src/game/rulesets/wftda_2025.py | 8 +++---- backend/src/game/series/models.py | 1 - backend/src/game/series/schemas.py | 30 ++++++++++++------------- backend/src/game/team_jams/models.py | 14 +++++++----- backend/src/game/team_jams/schemas.py | 3 +-- backend/src/game/teams/models.py | 14 +++++++----- backend/src/game/teams/schemas.py | 4 +--- backend/src/game/timeouts/models.py | 22 +++++++++--------- backend/src/game/timeouts/schemas.py | 7 +++--- backend/src/game/trip_events/models.py | 6 ++--- 17 files changed, 80 insertions(+), 82 deletions(-) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 10d57c8d..7eee806b 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -32,8 +32,8 @@ class BaseBout(CacheableSQLModel): ruleset: ClassVar[Ruleset] - series_id: Mapped[int] = mapped_column(ForeignKey('series.id')) - clock_id: Mapped[int] = mapped_column(ForeignKey('clocks.id', ondelete='RESTRICT')) + series_id: Mapped[int] = mapped_column(ForeignKey('series._id')) + clock_id: Mapped[int] = mapped_column(ForeignKey('clocks._id', ondelete='RESTRICT')) start_countdown: Mapped[datetime | None] = mapped_column(default=None) is_final: Mapped[bool] = mapped_column(default=False) diff --git a/backend/src/game/bouts/schemas.py b/backend/src/game/bouts/schemas.py index 703c4144..f1682af7 100644 --- a/backend/src/game/bouts/schemas.py +++ b/backend/src/game/bouts/schemas.py @@ -10,14 +10,13 @@ from game.jams.schemas import JamSchema # noqa: TC002 from game.teams.schemas import TeamSchema # noqa: TC002 from game.timeouts.schemas import TimeoutSchema # noqa: TC002 -from pydantic import Field, computed_field +from pydantic import computed_field class BoutSchema(ServerSchema): """Represent a Bout as a JSON schema.""" - id: int - series_id: int + # FIXME: add Series UUID ruleset_name: str clock: ClockSchema is_running: bool @@ -25,12 +24,12 @@ class BoutSchema(ServerSchema): is_final: bool state: Literal['final', 'jam', 'lineup', 'stopped', 'timeout'] teams: list[TeamSchema] - jams: list[JamSchema] = Field(exclude=True) - timeouts: list[TimeoutSchema] = Field(exclude=True) + _jams: list[JamSchema] + _timeouts: list[TimeoutSchema] @computed_field @property - def jam_ids(self) -> list[list[int]]: + def jams(self) -> tuple[int, int, int]: """Get a list of lists representing the IDs of this Bout's Jams. Returns: @@ -39,18 +38,18 @@ def jam_ids(self) -> list[list[int]]: `bout.jam_ids[period_num][jam_num]`. """ - jam_ids: list[list[int]] = [[], [], []] - for jam in self.jams: - jam_ids[jam.period].append(jam.id) - return jam_ids + counts: dict[int, int] = {} + for jam in self._jams: + counts[jam.period] = counts.get(jam.period, 0) + 1 + return counts[0], counts[1], counts[2] @computed_field @property - def timeout_ids(self) -> list[int]: + def timeouts(self) -> int: """Get a list representing the IDs of this Bout's Timeouts. Returns: list[int]: the Timeout IDs of this Bout's Timeouts. """ - return [timeout.id for timeout in self.timeouts] + return len(self._timeouts) diff --git a/backend/src/game/clocks/schemas.py b/backend/src/game/clocks/schemas.py index ffb16a4c..3d458297 100644 --- a/backend/src/game/clocks/schemas.py +++ b/backend/src/game/clocks/schemas.py @@ -11,7 +11,6 @@ class ClockSchema(ServerSchema): """Represent a Clock as a JSON schema.""" - id: int start_timestamp: datetime | None elapsed: Annotated[timedelta, timedelta_serializer] alarm: Annotated[timedelta, timedelta_serializer] diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index 46f35881..5799ab04 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -29,7 +29,9 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): """An abstract Jam without any associated ruleset.""" - bout_id: Mapped[int | None] = mapped_column(ForeignKey('bouts.id'), nullable=False) + _bout_id: Mapped[int | None] = mapped_column( + ForeignKey('bouts._id'), nullable=False + ) num: Mapped[int] = mapped_column(index=True) period: Mapped[int] = mapped_column(index=True) @@ -38,7 +40,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): _bout: Mapped[BaseBout | None] = relationship( back_populates='jams', cascade=CASCADE_OTHER, - foreign_keys=[bout_id], + foreign_keys=[_bout_id], ) team_jams: Mapped[list[TeamJam]] = relationship( back_populates='_jam', @@ -47,7 +49,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): ) _ruleset: MappedSQLExpression[str] = column_property( - select(BaseBout.ruleset_name).where(BaseBout._id == bout_id).scalar_subquery() + select(BaseBout.ruleset_name).where(BaseBout._id == _bout_id).scalar_subquery() ) __tablename__: str = 'jams' @@ -57,7 +59,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): 'confirm_deleted_rows': False, } __table_args__: tuple[Constraint, ...] = AbstractOneShotModel.__table_args__ + ( - UniqueConstraint('bout_id', 'num', 'period'), + UniqueConstraint('_bout_id', 'num', 'period'), ) def __str__(self) -> str: @@ -67,7 +69,7 @@ def __str__(self) -> str: str: a str representation of this Jam. """ - return f'[Bout ID: {self.bout_id}, P{self.period} J{self.num}]' + return f'[Bout ID: {self._bout_id}, P{self.period} J{self.num}]' def __init__(self, period_num: int, jam_num: int, *team_jams: TeamJam) -> None: """Initialize a Jam. @@ -82,7 +84,7 @@ def __init__(self, period_num: int, jam_num: int, *team_jams: TeamJam) -> None: @override def cache_key(self) -> CacheKey: - return (self.__tablename__, self.bout_id, self.period, self.num) + return (self.__tablename__, self._bout_id, self.period, self.num) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: @@ -118,7 +120,7 @@ def get_team_jam(self, team: BaseTeam | int) -> TeamJam: # Get the first TeamJam that has the specified Team ID team_jam: TeamJam | None = next( - (tj for tj in self.team_jams if tj.team_id == team), None + (tj for tj in self.team_jams if tj._team_id == team), None ) if team_jam is None: diff --git a/backend/src/game/jams/schemas.py b/backend/src/game/jams/schemas.py index 020104b3..c5278c08 100644 --- a/backend/src/game/jams/schemas.py +++ b/backend/src/game/jams/schemas.py @@ -10,8 +10,7 @@ class JamSchema(ServerSchema): """Represent a Jam as a JSON schema.""" - id: int - bout_id: int + # FIXME: add Bout UUID period: int num: int diff --git a/backend/src/game/rosters/models.py b/backend/src/game/rosters/models.py index d4b70efc..b2709da0 100644 --- a/backend/src/game/rosters/models.py +++ b/backend/src/game/rosters/models.py @@ -13,7 +13,7 @@ class Skater(BaseSQLModel): """Represent a singular Skater in roller derby.""" - roster_id: Mapped[int] = mapped_column(ForeignKey('rosters.id')) + _roster_id: Mapped[int] = mapped_column(ForeignKey('rosters._id')) name: Mapped[str] = mapped_column() pronouns: Mapped[str] = mapped_column() # TODO: Implement pronouns number: Mapped[str] = mapped_column() @@ -21,7 +21,7 @@ class Skater(BaseSQLModel): _roster: Mapped[Roster] = relationship( back_populates='skaters', cascade=CASCADE_OTHER, - foreign_keys=[roster_id], + foreign_keys=[_roster_id], lazy='selectin', ) diff --git a/backend/src/game/rosters/schemas.py b/backend/src/game/rosters/schemas.py index 73e7ab90..09fa3065 100644 --- a/backend/src/game/rosters/schemas.py +++ b/backend/src/game/rosters/schemas.py @@ -6,5 +6,5 @@ class RosterSchema(ServerSchema): """Represent a Roster as a JSON schema.""" - id: int + # FIXME: add roster UUID name: str diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index 81f9cc1a..af40f222 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -335,14 +335,14 @@ class Timeout(_WFTDAModel, BaseTimeout): def set_type(self, is_review: bool) -> None: logging.info( f'Setting {self} to {"official review" if is_review else "timeout"} type ' - f'in Bout ID {self.bout_id}' + f'in Bout ID {self._bout_id}' ) self.is_review = is_review @override def set_team(self, team: BaseTeam | None) -> None: - if team is not None and team.bout_id != self.bout_id: + if team is not None and team._bout_id != self._bout_id: raise GameStateError('Team and timeout are not part of the same Bout') if team is None and self.is_review: raise GameRulesError('Official reviews can only be called by teams') @@ -350,7 +350,7 @@ def set_team(self, team: BaseTeam | None) -> None: logging.info( f'Setting Timeout ID {self._id} calling team to ' f'{f"Team ID {team._id}" if team is not None else "officials"} in Bout ID ' - f'{self.bout_id}' + f'{self._bout_id}' ) self.team = team @@ -360,7 +360,7 @@ def set_team(self, team: BaseTeam | None) -> None: def set_retained(self, retained: bool) -> None: logging.info( f'Setting {self} to {"" if retained else "un"}retained in Bout ID ' - f'{self.bout_id}' + f'{self._bout_id}' ) self.retained = retained diff --git a/backend/src/game/series/models.py b/backend/src/game/series/models.py index dc4778ea..b5e17b95 100644 --- a/backend/src/game/series/models.py +++ b/backend/src/game/series/models.py @@ -21,7 +21,6 @@ class Series(CacheableSQLModel): """ - rowid: Mapped[int] = mapped_column(system=True) name: Mapped[str] = mapped_column(default='') bouts: Mapped[list[BaseBout]] = relationship( diff --git a/backend/src/game/series/schemas.py b/backend/src/game/series/schemas.py index b2371242..b55d1a65 100644 --- a/backend/src/game/series/schemas.py +++ b/backend/src/game/series/schemas.py @@ -2,28 +2,28 @@ from core import ServerSchema from game.bouts.schemas import BoutSchema -from pydantic import Field, computed_field class SeriesSchema(ServerSchema): """Represent a Series as a JSON schema.""" - id: int + # FIXME: add Series UUID name: str - bouts: list[BoutSchema] = Field(exclude=True) + _bouts: list[BoutSchema] - @computed_field - @property - def bout_ids(self) -> list[int]: - """Get a list representing the IDs of this Series' Bouts.""" - return [bout.id for bout in self.bouts] + # FIXME: uncomment all of this + # @computed_field + # @property + # def bout_ids(self) -> list[int]: + # """Get a list representing the IDs of this Series' Bouts.""" + # return [bout.id for bout in self._bouts] - @computed_field - @property - def active_bout_id(self) -> int | None: - """The active Bout ID of this Series or None if there is no active Bout. + # @computed_field + # @property + # def active_bout_id(self) -> int | None: + # """The active Bout ID of this Series or None if there is no active Bout. - The active Bout is the first Bout in the Series which is not final. + # The active Bout is the first Bout in the Series which is not final. - """ - return next((bout.id for bout in self.bouts if not bout.is_final), None) + # """ + # return next((bout.id for bout in self._bouts if not bout.is_final), None) diff --git a/backend/src/game/team_jams/models.py b/backend/src/game/team_jams/models.py index 6e1d85e0..e249fe93 100644 --- a/backend/src/game/team_jams/models.py +++ b/backend/src/game/team_jams/models.py @@ -32,18 +32,20 @@ class TeamJam(BaseSQLModel): """ - team_id: Mapped[int | None] = mapped_column(ForeignKey('teams.id'), nullable=False) - jam_id: Mapped[int | None] = mapped_column(ForeignKey('jams.id'), nullable=False) + _team_id: Mapped[int | None] = mapped_column( + ForeignKey('teams._id'), nullable=False + ) + _jam_id: Mapped[int | None] = mapped_column(ForeignKey('jams._id'), nullable=False) _team: Mapped[BaseTeam | None] = relationship( back_populates='team_jams', cascade=CASCADE_OTHER, - foreign_keys=[team_id], + foreign_keys=[_team_id], ) _jam: Mapped[BaseJam] = relationship( back_populates='team_jams', cascade=CASCADE_OTHER, - foreign_keys=[jam_id], + foreign_keys=[_jam_id], ) events: Mapped[list[TripEvent]] = relationship( back_populates='_team_jam', @@ -53,10 +55,10 @@ class TeamJam(BaseSQLModel): ) jam_num: MappedSQLExpression[int] = column_property( - select(BaseJam.num).where(BaseJam._id == jam_id).scalar_subquery() + select(BaseJam.num).where(BaseJam._id == _jam_id).scalar_subquery() ) period_num: MappedSQLExpression[int] = column_property( - select(BaseJam.period).where(BaseJam._id == jam_id).scalar_subquery() + select(BaseJam.period).where(BaseJam._id == _jam_id).scalar_subquery() ) __tablename__: str = 'team_jams' diff --git a/backend/src/game/team_jams/schemas.py b/backend/src/game/team_jams/schemas.py index 81a6d209..5618189a 100644 --- a/backend/src/game/team_jams/schemas.py +++ b/backend/src/game/team_jams/schemas.py @@ -7,6 +7,5 @@ class TeamJamSchema(ServerSchema): """Represent a TeamJam as a JSON schema.""" - jam_id: int - team_id: int + # FIXME: figure out how to refer back to the team that owns this team-jam events: list[TripEventSchema] diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index 02c7bb2d..fec9a5dc 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -34,8 +34,10 @@ class BaseTeam(BaseSQLModel): data like a team's score offset. """ - roster_id: Mapped[int] = mapped_column(ForeignKey('rosters.id')) - bout_id: Mapped[int | None] = mapped_column(ForeignKey('bouts.id'), nullable=False) + _roster_id: Mapped[int] = mapped_column(ForeignKey('rosters._id')) + _bout_id: Mapped[int | None] = mapped_column( + ForeignKey('bouts._id'), nullable=False + ) # TODO: Implement Team colors score_offset: Mapped[int] = mapped_column(default=0) @@ -44,12 +46,12 @@ class BaseTeam(BaseSQLModel): _roster: Mapped[Roster] = relationship( cascade=CASCADE_OTHER, - foreign_keys=[roster_id], + foreign_keys=[_roster_id], ) _bout: Mapped[BaseBout | None] = relationship( back_populates='teams', cascade=CASCADE_OTHER, - foreign_keys=[bout_id], + foreign_keys=[_bout_id], ) team_jams: Mapped[list[TeamJam]] = relationship( back_populates='_team', @@ -73,7 +75,7 @@ class BaseTeam(BaseSQLModel): ) _ruleset: MappedSQLExpression[str] = column_property( - select(BaseBout.ruleset_name).where(BaseBout._id == bout_id).scalar_subquery() + select(BaseBout.ruleset_name).where(BaseBout._id == _bout_id).scalar_subquery() ) __tablename__: str = 'teams' @@ -155,7 +157,7 @@ def jam_score(self) -> int: """ active_team_jam: TeamJam | None = next( - (tj for tj in self.team_jams if tj.jam_id == self._active_jam_id), None + (tj for tj in self.team_jams if tj._jam_id == self._active_jam_id), None ) if active_team_jam is None: return 0 diff --git a/backend/src/game/teams/schemas.py b/backend/src/game/teams/schemas.py index 5a08258e..8b84dbd9 100644 --- a/backend/src/game/teams/schemas.py +++ b/backend/src/game/teams/schemas.py @@ -8,9 +8,7 @@ class TeamSchema(ServerSchema): """Represent a Team as a JSON schema.""" - id: int - roster_id: int - bout_id: int + # FIXME: add Roster UUID bout_score: int jam_score: int timeouts_remaining: int diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index 864d4d02..4e335389 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -29,9 +29,9 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): Timeouts models can represent either a timeout or an official review. """ - bout_id: Mapped[int] = mapped_column(ForeignKey('bouts.id')) - team_id: Mapped[int | None] = mapped_column(ForeignKey('teams.id')) - jam_id: Mapped[int | None] = mapped_column(ForeignKey('jams.id')) + _bout_id: Mapped[int] = mapped_column(ForeignKey('bouts._id')) + _team_id: Mapped[int | None] = mapped_column(ForeignKey('teams._id')) + _jam_id: Mapped[int | None] = mapped_column(ForeignKey('jams._id')) num: Mapped[int] = mapped_column() clock_elapsed: Mapped[timedelta | None] = mapped_column(default=None) @@ -44,22 +44,22 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): _bout: Mapped[BaseBout] = relationship( back_populates='timeouts', cascade=CASCADE_OTHER, - foreign_keys=[bout_id], + foreign_keys=[_bout_id], ) # The next two relationships are special cases - they can be eagerly loaded team: Mapped[BaseTeam | None] = relationship( back_populates='timeouts', cascade=CASCADE_OTHER, - foreign_keys=[team_id], + foreign_keys=[_team_id], lazy='selectin', ) jam: Mapped[BaseJam | None] = relationship( - cascade=CASCADE_OTHER, foreign_keys=[jam_id], lazy='selectin' + cascade=CASCADE_OTHER, foreign_keys=[_jam_id], lazy='selectin' ) ruleset: MappedSQLExpression[str] = column_property( - select(BaseBout.ruleset_name).where(BaseBout._id == bout_id).scalar_subquery() + select(BaseBout.ruleset_name).where(BaseBout._id == _bout_id).scalar_subquery() ) __tablename__: str = 'timeouts' @@ -67,7 +67,7 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): 'polymorphic_abstract': True, 'polymorphic_on': ruleset, } - __table_args__: tuple[Constraint, ...] = (UniqueConstraint('bout_id', 'num'),) + __table_args__: tuple[Constraint, ...] = (UniqueConstraint('_bout_id', 'num'),) def __str__(self) -> str: """Return a str representation of this Timeout. @@ -76,7 +76,7 @@ def __str__(self) -> str: str: a str representation of this Timeout. """ - return f'[Bout ID: {self.bout_id}, T{self.num}]' + return f'[Bout ID: {self._bout_id}, T{self.num}]' def __init__(self, bout: BaseBout, num: int) -> None: """Initialize a Timeout. @@ -89,11 +89,11 @@ def __init__(self, bout: BaseBout, num: int) -> None: num (int): the unique Timeout number associated with this Bout. """ - super().__init__(_bout=bout, bout_id=bout._id, num=num) + super().__init__(_bout=bout, num=num) @override def cache_key(self) -> CacheKey: - return (self.__tablename__, self.bout_id, self._id) + return (self.__tablename__, self._bout_id, self._id) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/timeouts/schemas.py b/backend/src/game/timeouts/schemas.py index 490464a6..c97a7b8e 100644 --- a/backend/src/game/timeouts/schemas.py +++ b/backend/src/game/timeouts/schemas.py @@ -9,12 +9,11 @@ class TimeoutSchema(ServerSchema): """Represent a Timeout as a JSON schema.""" - id: int - bout_id: int + # FIXME: add Bout UUID num: int - team_id: int | None - jam_id: int | None + # team_id: int | None # FIXME: add a way to refer to team + # FIXME: add period num and jam num, if any start_timestamp: datetime | None stop_timestamp: datetime | None diff --git a/backend/src/game/trip_events/models.py b/backend/src/game/trip_events/models.py index 4353d014..2402deb1 100644 --- a/backend/src/game/trip_events/models.py +++ b/backend/src/game/trip_events/models.py @@ -26,8 +26,8 @@ class TripEvent(BaseSQLModel): eligibility. """ - team_jam_id: Mapped[int | None] = mapped_column( - ForeignKey('team_jams.id'), nullable=False + _team_jam_id: Mapped[int | None] = mapped_column( + ForeignKey('team_jams._id'), nullable=False ) timestamp: Mapped[datetime] = mapped_column() @@ -37,7 +37,7 @@ class TripEvent(BaseSQLModel): star_pass: Mapped[bool] = mapped_column(default=False) _team_jam: Mapped[TeamJam | None] = relationship( - back_populates='events', cascade=CASCADE_OTHER, foreign_keys=[team_jam_id] + back_populates='events', cascade=CASCADE_OTHER, foreign_keys=[_team_jam_id] ) __tablename__: str = 'trip_events' From 0c81eb5e7f272fbdd5d245a949f29394fe98df96 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 21 Jan 2026 19:00:48 -0800 Subject: [PATCH 005/105] make foreign keys private --- backend/src/game/bouts/models.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 7eee806b..16c7faae 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -32,8 +32,10 @@ class BaseBout(CacheableSQLModel): ruleset: ClassVar[Ruleset] - series_id: Mapped[int] = mapped_column(ForeignKey('series._id')) - clock_id: Mapped[int] = mapped_column(ForeignKey('clocks._id', ondelete='RESTRICT')) + _series_id: Mapped[int] = mapped_column(ForeignKey('series._id')) + _clock_id: Mapped[int] = mapped_column( + ForeignKey('clocks._id', ondelete='RESTRICT') + ) start_countdown: Mapped[datetime | None] = mapped_column(default=None) is_final: Mapped[bool] = mapped_column(default=False) @@ -43,11 +45,11 @@ class BaseBout(CacheableSQLModel): _series: Mapped[Series] = relationship( back_populates='bouts', cascade=CASCADE_OTHER, - foreign_keys=[series_id], + foreign_keys=[_series_id], ) clock: Mapped[Clock] = relationship( cascade=CASCADE_CHILD, - foreign_keys=[clock_id], + foreign_keys=[_clock_id], lazy='joined', single_parent=True, ) From 771b54d8243a1c3646ce4f048cb014d0fa487ea6 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 21 Jan 2026 19:19:41 -0800 Subject: [PATCH 006/105] make cache_key() awaitable --- backend/src/game/bouts/models.py | 2 +- backend/src/game/jams/models.py | 2 +- backend/src/game/models.py | 2 +- backend/src/game/rosters/models.py | 2 +- backend/src/game/series/models.py | 2 +- backend/src/game/timeouts/models.py | 2 +- backend/src/ws/service.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 16c7faae..aad2562c 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -98,7 +98,7 @@ def __init__(self, ruleset_name: str, *teams: BaseTeam) -> None: super().__init__(clock=Clock(), ruleset_name=ruleset_name, teams=list(teams)) @override - def cache_key(self) -> CacheKey: + async def cache_key(self) -> CacheKey: return (self.__tablename__, self._id) @override diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index 5799ab04..f1daacb6 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -83,7 +83,7 @@ def __init__(self, period_num: int, jam_num: int, *team_jams: TeamJam) -> None: super().__init__(period=period_num, num=jam_num, team_jams=list(team_jams)) @override - def cache_key(self) -> CacheKey: + async def cache_key(self) -> CacheKey: return (self.__tablename__, self._bout_id, self.period, self.num) @override diff --git a/backend/src/game/models.py b/backend/src/game/models.py index 5a42d50a..dc80255e 100644 --- a/backend/src/game/models.py +++ b/backend/src/game/models.py @@ -27,7 +27,7 @@ class CacheableSQLModel(BaseSQLModel): __abstract__: bool = True - def cache_key(self) -> CacheKey: + async def cache_key(self) -> CacheKey: """Get the cache key of this model. Return a unique cache key for this model which can be used by clients to cache diff --git a/backend/src/game/rosters/models.py b/backend/src/game/rosters/models.py index b2709da0..ddda578f 100644 --- a/backend/src/game/rosters/models.py +++ b/backend/src/game/rosters/models.py @@ -94,7 +94,7 @@ def __init__(self, name: str, league: str, mnemonic: str = '') -> None: super().__init__(name=name, league=league, mnemonic=mnemonic) @override - def cache_key(self) -> CacheKey: + async def cache_key(self) -> CacheKey: return (self.__tablename__, self._id) @override diff --git a/backend/src/game/series/models.py b/backend/src/game/series/models.py index b5e17b95..85d67d66 100644 --- a/backend/src/game/series/models.py +++ b/backend/src/game/series/models.py @@ -32,7 +32,7 @@ class Series(CacheableSQLModel): __tablename__: str = 'series' @override - def cache_key(self) -> CacheKey: + async def cache_key(self) -> CacheKey: # Special case where updating one Series invalidates the cache for all Series return (self.__tablename__, self._id) diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index 4e335389..b366a0fd 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -92,7 +92,7 @@ def __init__(self, bout: BaseBout, num: int) -> None: super().__init__(_bout=bout, num=num) @override - def cache_key(self) -> CacheKey: + async def cache_key(self) -> CacheKey: return (self.__tablename__, self._bout_id, self._id) @override diff --git a/backend/src/ws/service.py b/backend/src/ws/service.py index 0d00df23..c2e2e175 100644 --- a/backend/src/ws/service.py +++ b/backend/src/ws/service.py @@ -112,7 +112,7 @@ async def invalidate_queries(models: Iterable[BaseSQLModel]) -> None: if isinstance(parent, CacheableSQLModel) } cache_keys: list[CacheKey] = [ - cacheable.cache_key() + await cacheable.cache_key() for cacheable in cacheables if cacheable._id is not None ] From 33a1949ebe41d30ec0d8a40622290a12bd3916f5 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 21 Jan 2026 19:29:02 -0800 Subject: [PATCH 007/105] add uuid4 to appropriate models --- backend/src/game/bouts/models.py | 2 ++ backend/src/game/rosters/models.py | 3 +++ backend/src/game/series/models.py | 3 +++ 3 files changed, 8 insertions(+) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index aad2562c..109c5b87 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -4,6 +4,7 @@ from datetime import datetime # noqa: TC003 from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, final, override +from uuid import UUID, uuid4 from core import CASCADE_CHILD, CASCADE_OTHER from game.clocks.models import Clock @@ -36,6 +37,7 @@ class BaseBout(CacheableSQLModel): _clock_id: Mapped[int] = mapped_column( ForeignKey('clocks._id', ondelete='RESTRICT') ) + uuid: Mapped[UUID] = mapped_column(default_factory=uuid4, init=False) start_countdown: Mapped[datetime | None] = mapped_column(default=None) is_final: Mapped[bool] = mapped_column(default=False) diff --git a/backend/src/game/rosters/models.py b/backend/src/game/rosters/models.py index ddda578f..282896d4 100644 --- a/backend/src/game/rosters/models.py +++ b/backend/src/game/rosters/models.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import override +from uuid import UUID, uuid4 from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel from game.models import CacheableSQLModel, CacheKey @@ -58,6 +59,8 @@ async def get_roster(self) -> Roster: class Roster(CacheableSQLModel): """Represent a Roster of skaters.""" + uuid: Mapped[UUID] = mapped_column(default_factory=uuid4, init=False) + name: Mapped[str] = mapped_column() league: Mapped[str] = mapped_column() mnemonic: Mapped[str] = mapped_column() diff --git a/backend/src/game/series/models.py b/backend/src/game/series/models.py index 85d67d66..bce9cef2 100644 --- a/backend/src/game/series/models.py +++ b/backend/src/game/series/models.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, override +from uuid import UUID, uuid4 from core import CASCADE_CHILD, BaseSQLModel from game.models import CacheableSQLModel, CacheKey @@ -21,6 +22,8 @@ class Series(CacheableSQLModel): """ + uuid: Mapped[UUID] = mapped_column(default_factory=uuid4, init=False) + name: Mapped[str] = mapped_column(default='') bouts: Mapped[list[BaseBout]] = relationship( From 208b90fd27dcc2a88fb6f8d812412a124a2fd745 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 21 Jan 2026 20:14:30 -0800 Subject: [PATCH 008/105] fix uuid --- backend/src/game/bouts/models.py | 8 +++++--- backend/src/game/bouts/schemas.py | 19 ++++++++++--------- backend/src/game/rosters/models.py | 4 ++-- backend/src/game/rosters/schemas.py | 4 +++- backend/src/game/series/models.py | 11 ++++++++++- backend/src/game/series/schemas.py | 4 +++- 6 files changed, 33 insertions(+), 17 deletions(-) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 109c5b87..1f7c9eab 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -37,7 +37,7 @@ class BaseBout(CacheableSQLModel): _clock_id: Mapped[int] = mapped_column( ForeignKey('clocks._id', ondelete='RESTRICT') ) - uuid: Mapped[UUID] = mapped_column(default_factory=uuid4, init=False) + uuid: Mapped[UUID] = mapped_column() start_countdown: Mapped[datetime | None] = mapped_column(default=None) is_final: Mapped[bool] = mapped_column(default=False) @@ -70,7 +70,7 @@ class BaseBout(CacheableSQLModel): back_populates='_bout', cascade=CASCADE_CHILD, lazy='selectin', - order_by=[column('id')], + order_by=[column('_id')], ) __tablename__: str = 'bouts' @@ -97,7 +97,9 @@ def __init__(self, ruleset_name: str, *teams: BaseTeam) -> None: teams (tuple[BaseTeam, ...]): the teams which will compete in this Bout. """ - super().__init__(clock=Clock(), ruleset_name=ruleset_name, teams=list(teams)) + super().__init__( + uuid=uuid4(), clock=Clock(), ruleset_name=ruleset_name, teams=list(teams) + ) @override async def cache_key(self) -> CacheKey: diff --git a/backend/src/game/bouts/schemas.py b/backend/src/game/bouts/schemas.py index f1682af7..c527dfcf 100644 --- a/backend/src/game/bouts/schemas.py +++ b/backend/src/game/bouts/schemas.py @@ -4,19 +4,20 @@ from datetime import datetime # noqa: TC003 from typing import Literal # noqa: TC003 +from uuid import UUID # noqa: TC003 from core import ServerSchema from game.clocks.schemas import ClockSchema # noqa: TC002 from game.jams.schemas import JamSchema # noqa: TC002 from game.teams.schemas import TeamSchema # noqa: TC002 from game.timeouts.schemas import TimeoutSchema # noqa: TC002 -from pydantic import computed_field +from pydantic import Field, computed_field class BoutSchema(ServerSchema): """Represent a Bout as a JSON schema.""" - # FIXME: add Series UUID + uuid: UUID ruleset_name: str clock: ClockSchema is_running: bool @@ -24,12 +25,12 @@ class BoutSchema(ServerSchema): is_final: bool state: Literal['final', 'jam', 'lineup', 'stopped', 'timeout'] teams: list[TeamSchema] - _jams: list[JamSchema] - _timeouts: list[TimeoutSchema] + jams: list[JamSchema] = Field(exclude=True) + timeouts: list[TimeoutSchema] = Field(exclude=True) @computed_field @property - def jams(self) -> tuple[int, int, int]: + def jam_counts(self) -> tuple[int, int, int]: """Get a list of lists representing the IDs of this Bout's Jams. Returns: @@ -39,17 +40,17 @@ def jams(self) -> tuple[int, int, int]: """ counts: dict[int, int] = {} - for jam in self._jams: + for jam in self.jams: counts[jam.period] = counts.get(jam.period, 0) + 1 - return counts[0], counts[1], counts[2] + return counts.get(0, 0), counts.get(1, 0), counts.get(2, 0) @computed_field @property - def timeouts(self) -> int: + def timeout_counts(self) -> int: """Get a list representing the IDs of this Bout's Timeouts. Returns: list[int]: the Timeout IDs of this Bout's Timeouts. """ - return len(self._timeouts) + return len(self.timeouts) diff --git a/backend/src/game/rosters/models.py b/backend/src/game/rosters/models.py index 282896d4..d56341e9 100644 --- a/backend/src/game/rosters/models.py +++ b/backend/src/game/rosters/models.py @@ -59,7 +59,7 @@ async def get_roster(self) -> Roster: class Roster(CacheableSQLModel): """Represent a Roster of skaters.""" - uuid: Mapped[UUID] = mapped_column(default_factory=uuid4, init=False) + uuid: Mapped[UUID] = mapped_column() name: Mapped[str] = mapped_column() league: Mapped[str] = mapped_column() @@ -94,7 +94,7 @@ def __init__(self, name: str, league: str, mnemonic: str = '') -> None: mnemonic = mnemonic.strip() if mnemonic == '': pass # TODO: Implement team name mnemonic algorithm - super().__init__(name=name, league=league, mnemonic=mnemonic) + super().__init__(uuid=uuid4(), name=name, league=league, mnemonic=mnemonic) @override async def cache_key(self) -> CacheKey: diff --git a/backend/src/game/rosters/schemas.py b/backend/src/game/rosters/schemas.py index 09fa3065..dbaa6422 100644 --- a/backend/src/game/rosters/schemas.py +++ b/backend/src/game/rosters/schemas.py @@ -1,10 +1,12 @@ """Pydantic Roster schemas.""" +from uuid import UUID + from core import ServerSchema class RosterSchema(ServerSchema): """Represent a Roster as a JSON schema.""" - # FIXME: add roster UUID + uuid: UUID name: str diff --git a/backend/src/game/series/models.py b/backend/src/game/series/models.py index bce9cef2..8faf40e2 100644 --- a/backend/src/game/series/models.py +++ b/backend/src/game/series/models.py @@ -22,7 +22,7 @@ class Series(CacheableSQLModel): """ - uuid: Mapped[UUID] = mapped_column(default_factory=uuid4, init=False) + uuid: Mapped[UUID] = mapped_column() name: Mapped[str] = mapped_column(default='') @@ -34,6 +34,15 @@ class Series(CacheableSQLModel): __tablename__: str = 'series' + def __init__(self, name: str = '') -> None: + """Initialize a Series. + + Args: + name (str, optional): the name of the Series. Defaults to ''. + + """ + super().__init__(uuid=uuid4(), name=name) + @override async def cache_key(self) -> CacheKey: # Special case where updating one Series invalidates the cache for all Series diff --git a/backend/src/game/series/schemas.py b/backend/src/game/series/schemas.py index b55d1a65..07389b64 100644 --- a/backend/src/game/series/schemas.py +++ b/backend/src/game/series/schemas.py @@ -1,5 +1,7 @@ """Pydantic Series schemas.""" +from uuid import UUID + from core import ServerSchema from game.bouts.schemas import BoutSchema @@ -7,7 +9,7 @@ class SeriesSchema(ServerSchema): """Represent a Series as a JSON schema.""" - # FIXME: add Series UUID + uuid: UUID name: str _bouts: list[BoutSchema] From 27abe60959d87afbc8553ea628c9743d80fe5fb4 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 18:48:07 -0800 Subject: [PATCH 009/105] add get all bouts endpoint and cleanup tags --- backend/src/game/bouts/router.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/backend/src/game/bouts/router.py b/backend/src/game/bouts/router.py index aeb013df..12416923 100644 --- a/backend/src/game/bouts/router.py +++ b/backend/src/game/bouts/router.py @@ -1,13 +1,16 @@ """FastAPI routes associated with Bouts.""" from datetime import datetime -from typing import TYPE_CHECKING, Annotated, Final +from typing import TYPE_CHECKING, Annotated, Final, Sequence +from core import AsyncSessionDepends from fastapi import APIRouter, Body from game.rulesets.schemas import Ruleset from game.teams.dependencies import OptionalTeamDepends +from sqlalchemy import Result, Select, select from .dependencies import BoutDepends, _get_bout +from .models import BaseBout from .schemas import BoutSchema if TYPE_CHECKING: @@ -15,40 +18,48 @@ BOUTS_TAG = 'Bouts' -router: Final[APIRouter] = APIRouter(prefix='/bout') -router.add_api_route('', _get_bout, response_model=BoutSchema, tags=[BOUTS_TAG]) +router: Final[APIRouter] = APIRouter(prefix='/bout', tags=[BOUTS_TAG]) +router.add_api_route('', _get_bout, response_model=BoutSchema) -@router.get('/ruleset', response_model=Ruleset, tags=[BOUTS_TAG]) +@router.get('/bouts', response_model=list[BoutSchema]) +async def get_all_bouts(session: AsyncSessionDepends) -> Sequence[BaseBout]: + statement: Select[tuple[BaseBout]] = select(BaseBout) + results: Result[tuple[BaseBout]] = await session.execute(statement) + + return results.scalars().all() + + +@router.get('/ruleset', response_model=Ruleset) async def _get_ruleset(bout: BoutDepends) -> Ruleset: return bout.ruleset -@router.post('/beginPeriod', tags=[BOUTS_TAG]) +@router.post('/beginPeriod') async def begin_period(bout: BoutDepends) -> None: """Begin the period of the specified Bout.""" await bout.begin_period(datetime.now()) -@router.post('/endPeriod', tags=[BOUTS_TAG]) +@router.post('/endPeriod') async def end_period(bout: BoutDepends) -> None: """End the period of the specified Bout.""" await bout.end_period(datetime.now()) -@router.post('/startJam', tags=[BOUTS_TAG]) +@router.post('/startJam') async def start_jam(bout: BoutDepends) -> None: """Start the next Jam of the specified Bout.""" await bout.start_jam(datetime.now()) -@router.post('/stopJam', tags=[BOUTS_TAG]) +@router.post('/stopJam') async def stop_jam(bout: BoutDepends) -> None: """Stop the active Jam of the specified Bout.""" await bout.stop_jam(datetime.now()) -@router.post('/startTimeout', tags=[BOUTS_TAG]) +@router.post('/startTimeout') async def start_timeout( bout: BoutDepends, team: OptionalTeamDepends = None, # TODO: dependency should be in Body @@ -61,7 +72,7 @@ async def start_timeout( timeout.team = team -@router.post(path='/stopTimeout', tags=[BOUTS_TAG]) +@router.post(path='/stopTimeout') async def stop_timeout(bout: BoutDepends) -> None: """Stop the active Timeout in the specified Bout.""" await bout.stop_timeout(datetime.now()) From 26afa8cabd0980ceb9db83870ae4627ff48bcc8a Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 18:51:55 -0800 Subject: [PATCH 010/105] docs --- backend/src/game/bouts/schemas.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/backend/src/game/bouts/schemas.py b/backend/src/game/bouts/schemas.py index c527dfcf..71212c15 100644 --- a/backend/src/game/bouts/schemas.py +++ b/backend/src/game/bouts/schemas.py @@ -31,12 +31,10 @@ class BoutSchema(ServerSchema): @computed_field @property def jam_counts(self) -> tuple[int, int, int]: - """Get a list of lists representing the IDs of this Bout's Jams. + """A tuple representing the number of jams in this Bout, per Period. Returns: - list[list[int]]: the Jam IDs of this Bout's Jams. Each list represents a - Period such that a specific Jam may be queried using - `bout.jam_ids[period_num][jam_num]`. + tuple[int, int, int]: the number of Jams in each Period of this Bout. """ counts: dict[int, int] = {} @@ -50,7 +48,7 @@ def timeout_counts(self) -> int: """Get a list representing the IDs of this Bout's Timeouts. Returns: - list[int]: the Timeout IDs of this Bout's Timeouts. + list[int]: the number of timeouts in this Bout. """ return len(self.timeouts) From 7c72f49edd838f28ef0d5db67e616e488bb58862 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 19:07:07 -0800 Subject: [PATCH 011/105] implement team num system --- backend/src/game/bouts/models.py | 1 + backend/src/game/rulesets/wftda_2025.py | 10 +++++++--- backend/src/game/teams/models.py | 13 ++++++++++--- backend/src/game/teams/schemas.py | 1 + 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 1f7c9eab..f89a5e6b 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -59,6 +59,7 @@ class BaseBout(CacheableSQLModel): back_populates='_bout', cascade=CASCADE_CHILD, lazy='selectin', + order_by=[column('num')], ) jams: Mapped[list[BaseJam]] = relationship( back_populates='_bout', diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index af40f222..57955392 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -42,7 +42,11 @@ class Bout(_WFTDAModel, BaseBout): @override def __init__(self, home: Roster, away: Roster) -> None: - super().__init__(RULESET_NAME, Team(home), Team(away)) + super().__init__( + RULESET_NAME, + Team(home, 0), + Team(away, 1), + ) self.clock.alarm = timedelta(minutes=30) self.jams.append(Jam(0, 0, *[TeamJam(team) for team in self.teams])) self.timeouts.append(Timeout(self, 0)) @@ -208,8 +212,8 @@ class Team(_WFTDAModel, BaseTeam): """A Team model using the WFTDA 2025 ruleset.""" @override - def __init__(self, roster: Roster) -> None: - super().__init__(roster) + def __init__(self, roster: Roster, team_num: int) -> None: + super().__init__(roster, team_num) self.timeouts_remaining = Bout.ruleset.num_timeouts self.reviews_remaining = Bout.ruleset.num_reviews diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index fec9a5dc..fdf2ca98 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -9,7 +9,7 @@ from game.jams.models import BaseJam from game.team_jams.models import TeamJam from game.timeouts.models import BaseTimeout -from sqlalchemy import ForeignKey, select +from sqlalchemy import Constraint, ForeignKey, UniqueConstraint, select from sqlalchemy.orm import ( Mapped, MappedSQLExpression, @@ -40,6 +40,7 @@ class BaseTeam(BaseSQLModel): ) # TODO: Implement Team colors + num: Mapped[int] = mapped_column() score_offset: Mapped[int] = mapped_column(default=0) timeouts_remaining: Mapped[int] = mapped_column() reviews_remaining: Mapped[int] = mapped_column() @@ -79,6 +80,10 @@ class BaseTeam(BaseSQLModel): ) __tablename__: str = 'teams' + __table_args__: tuple[Constraint, ...] = ( + UniqueConstraint('_bout_id', '_roster_id'), + UniqueConstraint('_bout_id', 'num'), + ) __mapper_args__: dict[str, Any] = { 'polymorphic_abstract': True, 'polymorphic_on': _ruleset, @@ -99,15 +104,17 @@ def get_team_jam_score(cls, team_jam: TeamJam) -> int: """ raise NotImplementedError('BaseTeam.get_team_jam_score() must be overridden') - def __init__(self, roster: Roster) -> None: + def __init__(self, roster: Roster, team_num: int) -> None: """Initialize a Team. Args: bout (BaseBout): the owning Bout of the Team. roster (Roster): the Roster that this Team will use. + team_num (int): the Team number in the Bout. Each Team in a Bout must have + a unique team number. A 0 represents the home Team of a Bout. """ - super().__init__(_roster=roster) + super().__init__(_roster=roster, num=team_num) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/teams/schemas.py b/backend/src/game/teams/schemas.py index 8b84dbd9..a1e07cd2 100644 --- a/backend/src/game/teams/schemas.py +++ b/backend/src/game/teams/schemas.py @@ -9,6 +9,7 @@ class TeamSchema(ServerSchema): """Represent a Team as a JSON schema.""" # FIXME: add Roster UUID + num: int bout_score: int jam_score: int timeouts_remaining: int From 4b14eb7c385688cf71c694fa85c888fec78424c6 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 19:34:14 -0800 Subject: [PATCH 012/105] update series schema and add all bouts endpoint --- backend/src/game/bouts/router.py | 2 +- backend/src/game/series/router.py | 20 +++++++++++++++----- backend/src/game/series/schemas.py | 28 ++++++++++++++-------------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/backend/src/game/bouts/router.py b/backend/src/game/bouts/router.py index 12416923..ad71d310 100644 --- a/backend/src/game/bouts/router.py +++ b/backend/src/game/bouts/router.py @@ -22,7 +22,7 @@ router.add_api_route('', _get_bout, response_model=BoutSchema) -@router.get('/bouts', response_model=list[BoutSchema]) +@router.get('/allBouts', response_model=list[BoutSchema]) async def get_all_bouts(session: AsyncSessionDepends) -> Sequence[BaseBout]: statement: Select[tuple[BaseBout]] = select(BaseBout) results: Result[tuple[BaseBout]] = await session.execute(statement) diff --git a/backend/src/game/series/router.py b/backend/src/game/series/router.py index cc0a2148..bb207f95 100644 --- a/backend/src/game/series/router.py +++ b/backend/src/game/series/router.py @@ -1,15 +1,25 @@ """FastAPI routes associated with Series.""" -from typing import Final +from typing import Final, Sequence +from core import AsyncSessionDepends from fastapi import APIRouter +from sqlalchemy import Result, Select, select from .dependencies import _get_all_series +from .models import Series from .schemas import SeriesSchema SERIES_TAG = 'Series' -router: Final[APIRouter] = APIRouter(prefix='/series') -router.add_api_route( - '', _get_all_series, response_model=list[SeriesSchema], tags=[SERIES_TAG] -) +router: Final[APIRouter] = APIRouter(prefix='/series', tags=[SERIES_TAG]) +router.add_api_route('', _get_all_series, response_model=list[SeriesSchema]) + + +@router.get('/allSeries', response_model=list[SeriesSchema]) +async def get_all_series(session: AsyncSessionDepends) -> Sequence[Series]: + """Get all Series in the database.""" + statement: Select[tuple[Series]] = select(Series) + results: Result[tuple[Series]] = await session.execute(statement) + + return results.scalars().all() diff --git a/backend/src/game/series/schemas.py b/backend/src/game/series/schemas.py index 07389b64..071cfd27 100644 --- a/backend/src/game/series/schemas.py +++ b/backend/src/game/series/schemas.py @@ -4,6 +4,7 @@ from core import ServerSchema from game.bouts.schemas import BoutSchema +from pydantic import Field, computed_field class SeriesSchema(ServerSchema): @@ -11,21 +12,20 @@ class SeriesSchema(ServerSchema): uuid: UUID name: str - _bouts: list[BoutSchema] + bouts: list[BoutSchema] = Field(exclude=True) - # FIXME: uncomment all of this - # @computed_field - # @property - # def bout_ids(self) -> list[int]: - # """Get a list representing the IDs of this Series' Bouts.""" - # return [bout.id for bout in self._bouts] + @computed_field + @property + def bout_uuids(self) -> list[UUID]: + """Get a list representing the IDs of this Series' Bouts.""" + return [bout.uuid for bout in self.bouts] - # @computed_field - # @property - # def active_bout_id(self) -> int | None: - # """The active Bout ID of this Series or None if there is no active Bout. + @computed_field + @property + def active_bout_index(self) -> int | None: + """The active Bout ID of this Series or None if there is no active Bout. - # The active Bout is the first Bout in the Series which is not final. + The active Bout is the first Bout in the Series which is not final. - # """ - # return next((bout.id for bout in self._bouts if not bout.is_final), None) + """ + return next((i for i, bout in enumerate(self.bouts) if not bout.is_final), None) From 25c78c632f422eb5c13d80db4f1c50b589732cf2 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 19:37:47 -0800 Subject: [PATCH 013/105] get Bout by UUID --- backend/src/game/bouts/dependencies.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/game/bouts/dependencies.py b/backend/src/game/bouts/dependencies.py index d8eab8ba..ad259745 100644 --- a/backend/src/game/bouts/dependencies.py +++ b/backend/src/game/bouts/dependencies.py @@ -1,6 +1,7 @@ """The FastAPI dependencies methods for Bouts.""" from typing import TYPE_CHECKING, Annotated, TypeAlias +from uuid import UUID from core import AsyncSessionDepends from core.exceptions import ModelLookupError @@ -19,16 +20,16 @@ async def _get_bout( request: Request, user: GetUser, session: AsyncSessionDepends, - bout_id: Annotated[int, Query(alias='boutId')], + uuid: Annotated[UUID, Query()], ) -> BaseBout: # Query the database for the desired Bout - statement: Select[tuple[BaseBout]] = select(BaseBout).where(BaseBout._id == bout_id) + statement: Select[tuple[BaseBout]] = select(BaseBout).where(BaseBout.uuid == uuid) results: Result[tuple[BaseBout]] = await session.execute(statement) try: bout: BaseBout = results.scalar_one() except NoResultFound as e: - raise ModelLookupError(f'Could not find Bout with ID {bout_id}') from e + raise ModelLookupError(f'Could not find Bout with UUID {uuid}') from e # Optionally take a snapshot of the Bout state and return the Bout if request.method != 'GET': From 3ff799231631a385be155fdb605a0a6a802c1087 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 19:39:50 -0800 Subject: [PATCH 014/105] index on uuid --- backend/src/game/bouts/models.py | 2 +- backend/src/game/rosters/models.py | 2 +- backend/src/game/series/models.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index f89a5e6b..0ad3c567 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -37,7 +37,7 @@ class BaseBout(CacheableSQLModel): _clock_id: Mapped[int] = mapped_column( ForeignKey('clocks._id', ondelete='RESTRICT') ) - uuid: Mapped[UUID] = mapped_column() + uuid: Mapped[UUID] = mapped_column(index=True) start_countdown: Mapped[datetime | None] = mapped_column(default=None) is_final: Mapped[bool] = mapped_column(default=False) diff --git a/backend/src/game/rosters/models.py b/backend/src/game/rosters/models.py index d56341e9..c9188ee0 100644 --- a/backend/src/game/rosters/models.py +++ b/backend/src/game/rosters/models.py @@ -59,7 +59,7 @@ async def get_roster(self) -> Roster: class Roster(CacheableSQLModel): """Represent a Roster of skaters.""" - uuid: Mapped[UUID] = mapped_column() + uuid: Mapped[UUID] = mapped_column(index=True) name: Mapped[str] = mapped_column() league: Mapped[str] = mapped_column() diff --git a/backend/src/game/series/models.py b/backend/src/game/series/models.py index 8faf40e2..5c091ff1 100644 --- a/backend/src/game/series/models.py +++ b/backend/src/game/series/models.py @@ -22,7 +22,7 @@ class Series(CacheableSQLModel): """ - uuid: Mapped[UUID] = mapped_column() + uuid: Mapped[UUID] = mapped_column(index=True) name: Mapped[str] = mapped_column(default='') From f737539c3ccce21cf7830250a7886de2ed2f09c8 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 20:05:33 -0800 Subject: [PATCH 015/105] make UUID primary key for all objects --- backend/src/core/database.py | 3 ++- backend/src/game/bouts/models.py | 20 +++++++++----------- backend/src/game/jams/models.py | 27 +++++++++++++++------------ backend/src/game/rosters/models.py | 2 +- backend/src/game/series/models.py | 7 ++----- backend/src/game/team_jams/models.py | 17 ++++++++++------- backend/src/game/teams/models.py | 20 +++++++++++--------- backend/src/game/timeouts/models.py | 21 ++++++++++++--------- backend/src/game/utils.py | 2 +- backend/src/ws/service.py | 2 +- 10 files changed, 64 insertions(+), 57 deletions(-) diff --git a/backend/src/core/database.py b/backend/src/core/database.py index abd7d011..4b3592ad 100644 --- a/backend/src/core/database.py +++ b/backend/src/core/database.py @@ -10,6 +10,7 @@ ClassVar, Final, ) +from uuid import UUID, uuid4 from sqlalchemy.engine import URL from sqlalchemy.ext.asyncio import ( @@ -45,7 +46,7 @@ class BaseSQLModel(AsyncAttrs, DeclarativeBase): """ - _id: Mapped[int | None] = mapped_column(nullable=False, primary_key=True) + uuid: Mapped[UUID] = mapped_column(default=uuid4, primary_key=True) __abstract__: bool = True __type_annotation_map__: dict = {timedelta: _TimedeltaAsMilliseconds} diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 0ad3c567..835bba68 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -4,7 +4,7 @@ from datetime import datetime # noqa: TC003 from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, final, override -from uuid import UUID, uuid4 +from uuid import UUID # noqa: TC003 from core import CASCADE_CHILD, CASCADE_OTHER from game.clocks.models import Clock @@ -33,11 +33,11 @@ class BaseBout(CacheableSQLModel): ruleset: ClassVar[Ruleset] - _series_id: Mapped[int] = mapped_column(ForeignKey('series._id')) - _clock_id: Mapped[int] = mapped_column( - ForeignKey('clocks._id', ondelete='RESTRICT') + _clock_uuid: Mapped[UUID] = mapped_column( + ForeignKey('clocks.uuid', ondelete='RESTRICT') ) uuid: Mapped[UUID] = mapped_column(index=True) + series_uuid: Mapped[UUID] = mapped_column(ForeignKey('series.uuid')) start_countdown: Mapped[datetime | None] = mapped_column(default=None) is_final: Mapped[bool] = mapped_column(default=False) @@ -47,11 +47,11 @@ class BaseBout(CacheableSQLModel): _series: Mapped[Series] = relationship( back_populates='bouts', cascade=CASCADE_OTHER, - foreign_keys=[_series_id], + foreign_keys=[series_uuid], ) clock: Mapped[Clock] = relationship( cascade=CASCADE_CHILD, - foreign_keys=[_clock_id], + foreign_keys=[_clock_uuid], lazy='joined', single_parent=True, ) @@ -87,7 +87,7 @@ def __str__(self) -> str: str: a str representation of this Bout. """ - return f'[Bout ID: {self._id}]' + return f'[Bout UUID: {self.uuid}]' def __init__(self, ruleset_name: str, *teams: BaseTeam) -> None: """Instantiate a Bout. @@ -98,13 +98,11 @@ def __init__(self, ruleset_name: str, *teams: BaseTeam) -> None: teams (tuple[BaseTeam, ...]): the teams which will compete in this Bout. """ - super().__init__( - uuid=uuid4(), clock=Clock(), ruleset_name=ruleset_name, teams=list(teams) - ) + super().__init__(clock=Clock(), ruleset_name=ruleset_name, teams=list(teams)) @override async def cache_key(self) -> CacheKey: - return (self.__tablename__, self._id) + return (self.__tablename__, self.uuid) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index f1daacb6..3c873784 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal, override +from uuid import UUID from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel from game.bouts.models import BaseBout @@ -29,8 +30,8 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): """An abstract Jam without any associated ruleset.""" - _bout_id: Mapped[int | None] = mapped_column( - ForeignKey('bouts._id'), nullable=False + _bout_uuid: Mapped[UUID | None] = mapped_column( + ForeignKey('bouts.uuid'), nullable=False ) num: Mapped[int] = mapped_column(index=True) @@ -40,7 +41,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): _bout: Mapped[BaseBout | None] = relationship( back_populates='jams', cascade=CASCADE_OTHER, - foreign_keys=[_bout_id], + foreign_keys=[_bout_uuid], ) team_jams: Mapped[list[TeamJam]] = relationship( back_populates='_jam', @@ -49,7 +50,9 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): ) _ruleset: MappedSQLExpression[str] = column_property( - select(BaseBout.ruleset_name).where(BaseBout._id == _bout_id).scalar_subquery() + select(BaseBout.ruleset_name) + .where(BaseBout.uuid == _bout_uuid) + .scalar_subquery() ) __tablename__: str = 'jams' @@ -69,7 +72,7 @@ def __str__(self) -> str: str: a str representation of this Jam. """ - return f'[Bout ID: {self._bout_id}, P{self.period} J{self.num}]' + return f'[Bout ID: {self._bout_uuid}, P{self.period} J{self.num}]' def __init__(self, period_num: int, jam_num: int, *team_jams: TeamJam) -> None: """Initialize a Jam. @@ -84,7 +87,7 @@ def __init__(self, period_num: int, jam_num: int, *team_jams: TeamJam) -> None: @override async def cache_key(self) -> CacheKey: - return (self.__tablename__, self._bout_id, self.period, self.num) + return (self.__tablename__, self._bout_uuid, self.period, self.num) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: @@ -99,11 +102,11 @@ async def get_bout(self) -> BaseBout: """ return await self.awaitable_attrs._bout - def get_team_jam(self, team: BaseTeam | int) -> TeamJam: + def get_team_jam(self, team: BaseTeam | UUID) -> TeamJam: """Get the TeamJam associated with the desired Team. Args: - team (BaseTeam | int): the Team or Team ID of the desired TeamJam. + team (BaseTeam | UUID): the Team or Team UUID of the desired TeamJam. Raises: KeyError: the specified Team is not persistent in the database. @@ -113,14 +116,14 @@ def get_team_jam(self, team: BaseTeam | int) -> TeamJam: TeamJam: the TeamJam associated with the desired Team. """ - if not isinstance(team, int): - if team._id is None: + if not isinstance(team, UUID): + if team.uuid is None: raise KeyError('this team does not exist') - team = team._id + team = team.uuid # Get the first TeamJam that has the specified Team ID team_jam: TeamJam | None = next( - (tj for tj in self.team_jams if tj._team_id == team), None + (tj for tj in self.team_jams if tj.team_uuid == team), None ) if team_jam is None: diff --git a/backend/src/game/rosters/models.py b/backend/src/game/rosters/models.py index c9188ee0..20832dfd 100644 --- a/backend/src/game/rosters/models.py +++ b/backend/src/game/rosters/models.py @@ -98,7 +98,7 @@ def __init__(self, name: str, league: str, mnemonic: str = '') -> None: @override async def cache_key(self) -> CacheKey: - return (self.__tablename__, self._id) + return (self.__tablename__, self.uuid) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/series/models.py b/backend/src/game/series/models.py index 5c091ff1..97ed776a 100644 --- a/backend/src/game/series/models.py +++ b/backend/src/game/series/models.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, override -from uuid import UUID, uuid4 from core import CASCADE_CHILD, BaseSQLModel from game.models import CacheableSQLModel, CacheKey @@ -22,8 +21,6 @@ class Series(CacheableSQLModel): """ - uuid: Mapped[UUID] = mapped_column(index=True) - name: Mapped[str] = mapped_column(default='') bouts: Mapped[list[BaseBout]] = relationship( @@ -41,12 +38,12 @@ def __init__(self, name: str = '') -> None: name (str, optional): the name of the Series. Defaults to ''. """ - super().__init__(uuid=uuid4(), name=name) + super().__init__(name=name) @override async def cache_key(self) -> CacheKey: # Special case where updating one Series invalidates the cache for all Series - return (self.__tablename__, self._id) + return (self.__tablename__, self.uuid) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/team_jams/models.py b/backend/src/game/team_jams/models.py index e249fe93..387f6e89 100644 --- a/backend/src/game/team_jams/models.py +++ b/backend/src/game/team_jams/models.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, override +from uuid import UUID # noqa: TC003 from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel from game.jams.models import BaseJam @@ -32,20 +33,22 @@ class TeamJam(BaseSQLModel): """ - _team_id: Mapped[int | None] = mapped_column( - ForeignKey('teams._id'), nullable=False + team_uuid: Mapped[UUID | None] = mapped_column( + ForeignKey('teams.uuid'), nullable=False + ) + jam_uuid: Mapped[UUID | None] = mapped_column( + ForeignKey('jams.uuid'), nullable=False ) - _jam_id: Mapped[int | None] = mapped_column(ForeignKey('jams._id'), nullable=False) _team: Mapped[BaseTeam | None] = relationship( back_populates='team_jams', cascade=CASCADE_OTHER, - foreign_keys=[_team_id], + foreign_keys=[team_uuid], ) _jam: Mapped[BaseJam] = relationship( back_populates='team_jams', cascade=CASCADE_OTHER, - foreign_keys=[_jam_id], + foreign_keys=[jam_uuid], ) events: Mapped[list[TripEvent]] = relationship( back_populates='_team_jam', @@ -55,10 +58,10 @@ class TeamJam(BaseSQLModel): ) jam_num: MappedSQLExpression[int] = column_property( - select(BaseJam.num).where(BaseJam._id == _jam_id).scalar_subquery() + select(BaseJam.num).where(BaseJam.uuid == jam_uuid).scalar_subquery() ) period_num: MappedSQLExpression[int] = column_property( - select(BaseJam.period).where(BaseJam._id == _jam_id).scalar_subquery() + select(BaseJam.period).where(BaseJam.uuid == jam_uuid).scalar_subquery() ) __tablename__: str = 'team_jams' diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index fdf2ca98..e6eecce4 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -34,8 +34,8 @@ class BaseTeam(BaseSQLModel): data like a team's score offset. """ - _roster_id: Mapped[int] = mapped_column(ForeignKey('rosters._id')) - _bout_id: Mapped[int | None] = mapped_column( + roster_uuid: Mapped[int] = mapped_column(ForeignKey('rosters.uuid')) + bout_uuid: Mapped[int | None] = mapped_column( ForeignKey('bouts._id'), nullable=False ) @@ -47,12 +47,12 @@ class BaseTeam(BaseSQLModel): _roster: Mapped[Roster] = relationship( cascade=CASCADE_OTHER, - foreign_keys=[_roster_id], + foreign_keys=[roster_uuid], ) _bout: Mapped[BaseBout | None] = relationship( back_populates='teams', cascade=CASCADE_OTHER, - foreign_keys=[_bout_id], + foreign_keys=[bout_uuid], ) team_jams: Mapped[list[TeamJam]] = relationship( back_populates='_team', @@ -68,15 +68,17 @@ class BaseTeam(BaseSQLModel): ) # Used to calculate the current Jam score - _active_jam_id: MappedSQLExpression[int | None] = column_property( - select(BaseJam._id) + # SQLAlchemy does not understand `is` keyword in WHERE clauses, thus ignore E711. + _active_jam_uuid: MappedSQLExpression[int | None] = column_property( + select(BaseJam.uuid) .where(BaseJam.start_timestamp != None) # noqa: E711 .order_by(desc(BaseJam.period), desc(BaseJam.num)) .scalar_subquery() ) - _ruleset: MappedSQLExpression[str] = column_property( - select(BaseBout.ruleset_name).where(BaseBout._id == _bout_id).scalar_subquery() + select(BaseBout.ruleset_name) + .where(BaseBout.uuid == bout_uuid) + .scalar_subquery() ) __tablename__: str = 'teams' @@ -164,7 +166,7 @@ def jam_score(self) -> int: """ active_team_jam: TeamJam | None = next( - (tj for tj in self.team_jams if tj._jam_id == self._active_jam_id), None + (tj for tj in self.team_jams if tj.jam_uuid == self._active_jam_uuid), None ) if active_team_jam is None: return 0 diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index b366a0fd..6474014f 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -4,6 +4,7 @@ from datetime import timedelta # noqa: TC003 from typing import TYPE_CHECKING, Any, override +from uuid import UUID # noqa: TC003 from core import CASCADE_OTHER, BaseSQLModel from game.bouts.models import BaseBout @@ -29,9 +30,9 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): Timeouts models can represent either a timeout or an official review. """ - _bout_id: Mapped[int] = mapped_column(ForeignKey('bouts._id')) - _team_id: Mapped[int | None] = mapped_column(ForeignKey('teams._id')) - _jam_id: Mapped[int | None] = mapped_column(ForeignKey('jams._id')) + _team_uuid: Mapped[UUID | None] = mapped_column(ForeignKey('teams.uuid')) + _jam_uuid: Mapped[UUID | None] = mapped_column(ForeignKey('jams.uuid')) + bout_uuid: Mapped[UUID] = mapped_column(ForeignKey('bouts.uuid')) num: Mapped[int] = mapped_column() clock_elapsed: Mapped[timedelta | None] = mapped_column(default=None) @@ -44,22 +45,24 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): _bout: Mapped[BaseBout] = relationship( back_populates='timeouts', cascade=CASCADE_OTHER, - foreign_keys=[_bout_id], + foreign_keys=[bout_uuid], ) # The next two relationships are special cases - they can be eagerly loaded team: Mapped[BaseTeam | None] = relationship( back_populates='timeouts', cascade=CASCADE_OTHER, - foreign_keys=[_team_id], + foreign_keys=[_team_uuid], lazy='selectin', ) jam: Mapped[BaseJam | None] = relationship( - cascade=CASCADE_OTHER, foreign_keys=[_jam_id], lazy='selectin' + cascade=CASCADE_OTHER, foreign_keys=[_jam_uuid], lazy='selectin' ) ruleset: MappedSQLExpression[str] = column_property( - select(BaseBout.ruleset_name).where(BaseBout._id == _bout_id).scalar_subquery() + select(BaseBout.ruleset_name) + .where(BaseBout.uuid == bout_uuid) + .scalar_subquery() ) __tablename__: str = 'timeouts' @@ -76,7 +79,7 @@ def __str__(self) -> str: str: a str representation of this Timeout. """ - return f'[Bout ID: {self._bout_id}, T{self.num}]' + return f'[Bout ID: {self.bout_uuid}, T{self.num}]' def __init__(self, bout: BaseBout, num: int) -> None: """Initialize a Timeout. @@ -93,7 +96,7 @@ def __init__(self, bout: BaseBout, num: int) -> None: @override async def cache_key(self) -> CacheKey: - return (self.__tablename__, self._bout_id, self._id) + return (self.__tablename__, self.bout_uuid, self.num) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/utils.py b/backend/src/game/utils.py index 4c8dff19..aeff3207 100644 --- a/backend/src/game/utils.py +++ b/backend/src/game/utils.py @@ -33,7 +33,7 @@ async def restore(self) -> Memento: # Query and detach the current state of the database object table: type[CacheableSQLModel] = self._detached_state_to_restore.__class__ statement: Select[tuple[CacheableSQLModel]] = select(table).where( - table._id == self._detached_state_to_restore._id + table.uuid == self._detached_state_to_restore.uuid ) results: Result[tuple[CacheableSQLModel]] = await session.execute(statement) current_state: CacheableSQLModel = results.scalar_one() diff --git a/backend/src/ws/service.py b/backend/src/ws/service.py index c2e2e175..4c70d990 100644 --- a/backend/src/ws/service.py +++ b/backend/src/ws/service.py @@ -114,7 +114,7 @@ async def invalidate_queries(models: Iterable[BaseSQLModel]) -> None: cache_keys: list[CacheKey] = [ await cacheable.cache_key() for cacheable in cacheables - if cacheable._id is not None + if cacheable.uuid is not None ] if len(cache_keys) == 0: return From 954af61a7bfd6c200a9bac341ad3511169186c36 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 20:12:17 -0800 Subject: [PATCH 016/105] make method unaware of database schema --- backend/src/game/jams/models.py | 16 ++++----- backend/src/game/rulesets/wftda_2025.py | 46 +++++++++++-------------- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index 3c873784..f64f43c3 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -143,33 +143,33 @@ def lead_is_declared(self) -> bool: return True return False - async def add_trip(self, team_id: int, timestamp: datetime, passes: int) -> None: + async def add_trip(self, team: BaseTeam, timestamp: datetime, passes: int) -> None: """Add a Jammer trip to the desired Team's TeamJam. Args: - team_id (int): the ID of the desired Team. + team (BaseTeam): the desired Team. timestamp (datetime): the timestamp at which to add the trip. passes (int): the number of passes the Jammer earned. """ ... - async def set_lead(self, team_id: int, timestamp: datetime, lead: bool) -> None: + async def set_lead(self, team: BaseTeam, timestamp: datetime, lead: bool) -> None: """Set the lead Jammer status for the desired Team. Args: - team_id (int): the ID of the desired Team. + team (BaseTeam): the desired Team. timestamp (datetime): the timestamp at which to set lead. lead (bool): True if the Jammer has been declared lead. """ ... - async def set_lost(self, team_id: int, timestamp: datetime, lost: bool) -> None: + async def set_lost(self, team: BaseTeam, timestamp: datetime, lost: bool) -> None: """Set the lead eligibility for the desired Team. Args: - team_id (int): the ID of the desired Team. + team (BaseTeam): the desired Team. timestamp (datetime): the timestamp at which to set lead eligibility. lost (bool): True if the Jammer has lost lead eligibility. @@ -177,12 +177,12 @@ async def set_lost(self, team_id: int, timestamp: datetime, lost: bool) -> None: ... async def set_star_pass( - self, team_id: int, timestamp: datetime, star_pass: bool + self, team: BaseTeam, timestamp: datetime, star_pass: bool ) -> None: """Add a star pass to the desired Team. Args: - team_id (int): the ID of the desired Team. + team (BaseTeam): the desired Team. timestamp (datetime): the timestamp at which to add the star pass. star_pass (bool): True if the star has been successfully passed. diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index 57955392..a3cf65de 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -231,25 +231,25 @@ class Jam(_WFTDAModel, BaseJam): """A Jam model using the WFTDA 2025 ruleset.""" @override - async def add_trip(self, team_id: int, timestamp: datetime, passes: int) -> None: - team_jam: TeamJam = self.get_team_jam(team_id) + async def add_trip(self, team: BaseTeam, timestamp: datetime, passes: int) -> None: + team_jam: TeamJam = self.get_team_jam(team) - logging.info(f'Adding {passes} passes to Team ID {team_id} in {self}') + logging.info(f'Adding {passes} passes to {team} in {self}') is_initial: bool = len(team_jam.events) == 0 is_overtime: bool = self.period >= Bout.ruleset.num_periods if is_initial: - logging.info(f'This is the initial pass for Team ID {team_id} in {self}') + logging.info(f'This is the initial pass for {team} in {self}') event: TripEvent = TripEvent(timestamp, passes=passes) # Automatically set lead on the first 4-point trip if not self.lead_is_declared() and passes == Bout.ruleset.points_per_trip: - await self.set_lead(team_id, timestamp, True) + await self.set_lead(team, timestamp, True) # Lose eligibility on initial no-pass/no-penalty if len(team_jam.events) == 0 and passes < Bout.ruleset.points_per_trip: - await self.set_lost(team_id, timestamp, True) + await self.set_lost(team, timestamp, True) # Jammer cannot earn points on the initial pass if is_initial and not is_overtime: @@ -258,12 +258,10 @@ async def add_trip(self, team_id: int, timestamp: datetime, passes: int) -> None team_jam.events.append(event) @override - async def set_lead(self, team_id: int, timestamp: datetime, lead: bool) -> None: - team_jam: TeamJam = self.get_team_jam(team_id) + async def set_lead(self, team: BaseTeam, timestamp: datetime, lead: bool) -> None: + team_jam: TeamJam = self.get_team_jam(team) - logging.info( - f'{"Setting" if lead else "Unsetting"} lead for Team ID {team_id} in {self}' - ) + logging.info(f'{"Setting" if lead else "Unsetting"} lead for {team} in {self}') if lead: # Add a new Trip Event in which lead is declared @@ -280,12 +278,10 @@ async def set_lead(self, team_id: int, timestamp: datetime, lead: bool) -> None: team_jam.events.remove(event) @override - async def set_lost(self, team_id: int, timestamp: datetime, lost: bool) -> None: - team_jam: TeamJam = self.get_team_jam(team_id) + async def set_lost(self, team: BaseTeam, timestamp: datetime, lost: bool) -> None: + team_jam: TeamJam = self.get_team_jam(team) - logging.info( - f'{"Setting" if lost else "Unsetting"} lost for Team ID {team_id} in {self}' - ) + logging.info(f'{"Setting" if lost else "Unsetting"} lost for {team} in {self}') if lost: # Add a new Trip Event in which the Jammer has lost eligibility for lead @@ -303,13 +299,12 @@ async def set_lost(self, team_id: int, timestamp: datetime, lost: bool) -> None: @override async def set_star_pass( - self, team_id: int, timestamp: datetime, star_pass: bool + self, team: BaseTeam, timestamp: datetime, star_pass: bool ) -> None: - team_jam: TeamJam = self.get_team_jam(team_id) + team_jam: TeamJam = self.get_team_jam(team) logging.info( - f'{"Setting" if star_pass else "Unsetting"} star pass for Team ID ' - f'{team_id} in {self}' + f'{"Setting" if star_pass else "Unsetting"} star pass {team} in {self}' ) if star_pass: @@ -339,22 +334,21 @@ class Timeout(_WFTDAModel, BaseTimeout): def set_type(self, is_review: bool) -> None: logging.info( f'Setting {self} to {"official review" if is_review else "timeout"} type ' - f'in Bout ID {self._bout_id}' + f'in Bout ID {self.bout_uuid}' ) self.is_review = is_review @override def set_team(self, team: BaseTeam | None) -> None: - if team is not None and team._bout_id != self._bout_id: + if team is not None and team.bout_uuid != self.bout_uuid: raise GameStateError('Team and timeout are not part of the same Bout') if team is None and self.is_review: raise GameRulesError('Official reviews can only be called by teams') logging.info( - f'Setting Timeout ID {self._id} calling team to ' - f'{f"Team ID {team._id}" if team is not None else "officials"} in Bout ID ' - f'{self._bout_id}' + f'Setting {self} calling team to {team or "officials"} in Bout ID ' + f'{self.bout_uuid}' ) self.team = team @@ -364,7 +358,7 @@ def set_team(self, team: BaseTeam | None) -> None: def set_retained(self, retained: bool) -> None: logging.info( f'Setting {self} to {"" if retained else "un"}retained in Bout ID ' - f'{self._bout_id}' + f'{self.bout_uuid}' ) self.retained = retained From 36a83c879093dc04eee9cc8bfab9f98b1566d965 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 20:39:14 -0800 Subject: [PATCH 017/105] update dependencies for simplification --- backend/src/game/bouts/dependencies.py | 2 +- backend/src/game/bouts/models.py | 1 - backend/src/game/bouts/router.py | 23 +++++----- backend/src/game/jams/dependencies.py | 28 +++++------- backend/src/game/jams/models.py | 2 +- backend/src/game/jams/router.py | 29 ++++++------ backend/src/game/rosters/dependencies.py | 11 +++-- backend/src/game/rosters/models.py | 9 ++-- backend/src/game/teams/dependencies.py | 54 +++++------------------ backend/src/game/teams/models.py | 6 +-- backend/src/game/timeouts/dependencies.py | 26 ++++------- backend/src/game/timeouts/models.py | 2 +- backend/src/game/timeouts/router.py | 16 +++---- 13 files changed, 78 insertions(+), 131 deletions(-) diff --git a/backend/src/game/bouts/dependencies.py b/backend/src/game/bouts/dependencies.py index ad259745..a08a6671 100644 --- a/backend/src/game/bouts/dependencies.py +++ b/backend/src/game/bouts/dependencies.py @@ -37,4 +37,4 @@ async def _get_bout( return bout -BoutDepends: TypeAlias = Annotated[BaseBout, Depends(_get_bout)] +GetBout: TypeAlias = Annotated[BaseBout, Depends(_get_bout)] diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 835bba68..8d79c3d6 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -36,7 +36,6 @@ class BaseBout(CacheableSQLModel): _clock_uuid: Mapped[UUID] = mapped_column( ForeignKey('clocks.uuid', ondelete='RESTRICT') ) - uuid: Mapped[UUID] = mapped_column(index=True) series_uuid: Mapped[UUID] = mapped_column(ForeignKey('series.uuid')) start_countdown: Mapped[datetime | None] = mapped_column(default=None) diff --git a/backend/src/game/bouts/router.py b/backend/src/game/bouts/router.py index ad71d310..90770a2d 100644 --- a/backend/src/game/bouts/router.py +++ b/backend/src/game/bouts/router.py @@ -6,10 +6,9 @@ from core import AsyncSessionDepends from fastapi import APIRouter, Body from game.rulesets.schemas import Ruleset -from game.teams.dependencies import OptionalTeamDepends from sqlalchemy import Result, Select, select -from .dependencies import BoutDepends, _get_bout +from .dependencies import GetBout, _get_bout from .models import BaseBout from .schemas import BoutSchema @@ -31,49 +30,49 @@ async def get_all_bouts(session: AsyncSessionDepends) -> Sequence[BaseBout]: @router.get('/ruleset', response_model=Ruleset) -async def _get_ruleset(bout: BoutDepends) -> Ruleset: +async def _get_ruleset(bout: GetBout) -> Ruleset: return bout.ruleset @router.post('/beginPeriod') -async def begin_period(bout: BoutDepends) -> None: +async def begin_period(bout: GetBout) -> None: """Begin the period of the specified Bout.""" await bout.begin_period(datetime.now()) @router.post('/endPeriod') -async def end_period(bout: BoutDepends) -> None: +async def end_period(bout: GetBout) -> None: """End the period of the specified Bout.""" await bout.end_period(datetime.now()) @router.post('/startJam') -async def start_jam(bout: BoutDepends) -> None: +async def start_jam(bout: GetBout) -> None: """Start the next Jam of the specified Bout.""" await bout.start_jam(datetime.now()) @router.post('/stopJam') -async def stop_jam(bout: BoutDepends) -> None: +async def stop_jam(bout: GetBout) -> None: """Stop the active Jam of the specified Bout.""" await bout.stop_jam(datetime.now()) @router.post('/startTimeout') async def start_timeout( - bout: BoutDepends, - team: OptionalTeamDepends = None, # TODO: dependency should be in Body + bout: GetBout, + team_num: Annotated[int | None, Body(alias='teamNum')] = None, is_review: Annotated[bool, Body(alias='isReview')] = False, ) -> None: """Start a new Timeout in the specified Bout.""" timeout: BaseTimeout = await bout.start_timeout(datetime.now()) timeout.is_review = is_review - if team is not None: - timeout.team = team + if team_num is not None: + timeout.team = bout.teams[team_num] @router.post(path='/stopTimeout') -async def stop_timeout(bout: BoutDepends) -> None: +async def stop_timeout(bout: GetBout) -> None: """Stop the active Timeout in the specified Bout.""" await bout.stop_timeout(datetime.now()) diff --git a/backend/src/game/jams/dependencies.py b/backend/src/game/jams/dependencies.py index f8c267ca..0183afb7 100644 --- a/backend/src/game/jams/dependencies.py +++ b/backend/src/game/jams/dependencies.py @@ -1,34 +1,28 @@ """The FastAPI dependencies methods for Jams.""" -from typing import TYPE_CHECKING, Annotated, TypeAlias +from typing import Annotated, TypeAlias -from core import AsyncSessionDepends from core.exceptions import ModelLookupError from fastapi import Depends, Query, Request -from sqlalchemy import select -from sqlalchemy.exc import NoResultFound +from game.bouts.dependencies import GetBout from user import GetUser from .models import BaseJam -if TYPE_CHECKING: - from sqlalchemy.engine.result import Result - from sqlalchemy.sql.selectable import Select - async def _get_jam( request: Request, user: GetUser, - session: AsyncSessionDepends, - jam_id: Annotated[int, Query(alias='jamId')], + bout: GetBout, + period: Annotated[int, Query()], + num: Annotated[int, Query()], ) -> BaseJam: - statement: Select[tuple[BaseJam]] = select(BaseJam).where(BaseJam._id == jam_id) - results: Result[tuple[BaseJam]] = await session.execute(statement) - try: - jam: BaseJam = results.scalar_one() - except NoResultFound as e: - raise ModelLookupError(f'Could not find Jam with ID {jam_id}') from e + jam: BaseJam = next( + jam for jam in bout.jams if jam.period == period and jam.num == num + ) + except StopIteration as e: + raise ModelLookupError(f'Could not find P{period} J{num} in this Bout') from e if request.method != 'GET': user.stage(jam.get_memento()) @@ -36,4 +30,4 @@ async def _get_jam( return jam -GetJamByID: TypeAlias = Annotated[BaseJam, Depends(_get_jam)] +GetJam: TypeAlias = Annotated[BaseJam, Depends(_get_jam)] diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index f64f43c3..e1801f38 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -62,7 +62,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): 'confirm_deleted_rows': False, } __table_args__: tuple[Constraint, ...] = AbstractOneShotModel.__table_args__ + ( - UniqueConstraint('_bout_id', 'num', 'period'), + UniqueConstraint('_bout_uuid', 'num', 'period'), ) def __str__(self) -> str: diff --git a/backend/src/game/jams/router.py b/backend/src/game/jams/router.py index 8f27b82d..2f777d16 100644 --- a/backend/src/game/jams/router.py +++ b/backend/src/game/jams/router.py @@ -3,9 +3,10 @@ from datetime import datetime from typing import Annotated, Final -from fastapi import APIRouter, Body, Query +from fastapi import APIRouter, Body +from game.teams.dependencies import GetTeam -from .dependencies import GetJamByID, _get_jam +from .dependencies import GetJam, _get_jam from .schemas import JamSchema JAMS_TAG = 'Jams' @@ -16,39 +17,39 @@ @router.post('/addTrip', tags=[JAMS_TAG]) async def add_trip( - jam: GetJamByID, - team_id: Annotated[int, Query(alias='teamId')], + jam: GetJam, + team: GetTeam, passes: Annotated[int, Body()], ) -> None: """Add a Trip for the specified Team of the specified Jam.""" - await jam.add_trip(team_id, datetime.now(), passes) + await jam.add_trip(team, datetime.now(), passes) @router.post('/setLead', tags=[JAMS_TAG]) async def set_lead( - jam: GetJamByID, - team_id: Annotated[int, Query(alias='teamId')], + jam: GetJam, + team: GetTeam, lead: Annotated[bool, Body()], ) -> None: """Set Lead for the specified Team of the specified Jam.""" - await jam.set_lead(team_id, datetime.now(), lead) + await jam.set_lead(team, datetime.now(), lead) @router.post('/setLost', tags=[JAMS_TAG]) async def set_lost( - jam: GetJamByID, - team_id: Annotated[int, Query(alias='teamId')], + jam: GetJam, + team: GetTeam, lost: Annotated[bool, Body()], ) -> None: """Set Lost for the specified Team of the specified Jam.""" - await jam.set_lost(team_id, datetime.now(), lost) + await jam.set_lost(team, datetime.now(), lost) @router.post('/setStarPass', tags=[JAMS_TAG]) async def set_star_pass( - jam: GetJamByID, - team_id: Annotated[int, Query(alias='teamId')], + jam: GetJam, + team: GetTeam, star_pass: Annotated[bool, Body()], ) -> None: """Set a Star Pass for the specified Team of the specified Jam.""" - await jam.set_star_pass(team_id, datetime.now(), star_pass) + await jam.set_star_pass(team, datetime.now(), star_pass) diff --git a/backend/src/game/rosters/dependencies.py b/backend/src/game/rosters/dependencies.py index bcd7c0ab..b092690f 100644 --- a/backend/src/game/rosters/dependencies.py +++ b/backend/src/game/rosters/dependencies.py @@ -5,7 +5,7 @@ from core import AsyncSessionDepends from core.exceptions import ModelLookupError from fastapi import Depends, Query -from sqlalchemy import Result, select +from sqlalchemy import Result, Select, select from sqlalchemy.exc import NoResultFound from .models import Roster @@ -13,16 +13,15 @@ async def _get_roster( session: AsyncSessionDepends, - roster_id: Annotated[int, Query(alias='rosterId')], + uuid: Annotated[int, Query(alias='rosterId')], ) -> Roster: - results: Result[tuple[Roster]] = await session.execute( - select(Roster).where(Roster._id == roster_id) - ) + statement: Select[tuple[Roster]] = select(Roster).where(Roster.uuid == uuid) + results: Result[tuple[Roster]] = await session.execute(statement) try: roster: Roster = results.scalar_one() except NoResultFound as e: - raise ModelLookupError(f'Could not find Roster with ID {roster_id}') from e + raise ModelLookupError(f'Could not find Roster with UUID {uuid}') from e # TODO: handle mementos diff --git a/backend/src/game/rosters/models.py b/backend/src/game/rosters/models.py index 20832dfd..6e26b160 100644 --- a/backend/src/game/rosters/models.py +++ b/backend/src/game/rosters/models.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import override -from uuid import UUID, uuid4 from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel from game.models import CacheableSQLModel, CacheKey @@ -14,7 +13,7 @@ class Skater(BaseSQLModel): """Represent a singular Skater in roller derby.""" - _roster_id: Mapped[int] = mapped_column(ForeignKey('rosters._id')) + roster_uuid: Mapped[int] = mapped_column(ForeignKey('rosters.uuid')) name: Mapped[str] = mapped_column() pronouns: Mapped[str] = mapped_column() # TODO: Implement pronouns number: Mapped[str] = mapped_column() @@ -22,7 +21,7 @@ class Skater(BaseSQLModel): _roster: Mapped[Roster] = relationship( back_populates='skaters', cascade=CASCADE_OTHER, - foreign_keys=[_roster_id], + foreign_keys=[roster_uuid], lazy='selectin', ) @@ -59,8 +58,6 @@ async def get_roster(self) -> Roster: class Roster(CacheableSQLModel): """Represent a Roster of skaters.""" - uuid: Mapped[UUID] = mapped_column(index=True) - name: Mapped[str] = mapped_column() league: Mapped[str] = mapped_column() mnemonic: Mapped[str] = mapped_column() @@ -94,7 +91,7 @@ def __init__(self, name: str, league: str, mnemonic: str = '') -> None: mnemonic = mnemonic.strip() if mnemonic == '': pass # TODO: Implement team name mnemonic algorithm - super().__init__(uuid=uuid4(), name=name, league=league, mnemonic=mnemonic) + super().__init__(name=name, league=league, mnemonic=mnemonic) @override async def cache_key(self) -> CacheKey: diff --git a/backend/src/game/teams/dependencies.py b/backend/src/game/teams/dependencies.py index 8ef5b51e..13536fbc 100644 --- a/backend/src/game/teams/dependencies.py +++ b/backend/src/game/teams/dependencies.py @@ -1,51 +1,21 @@ """The FastAPI dependencies methods for Teams.""" -from typing import TYPE_CHECKING, Annotated, TypeAlias +from typing import Annotated, TypeAlias -from core import AsyncSessionDepends -from fastapi import Body, Depends, Query, Request -from sqlalchemy import select -from user import GetUser +from core.exceptions import ModelLookupError +from fastapi import Depends, Query +from game.bouts.dependencies import GetBout from .models import BaseTeam -if TYPE_CHECKING: - from sqlalchemy import Result, Select +async def _get_team(bout: GetBout, team_num: Annotated[int, Query()]) -> BaseTeam: + try: + return bout.teams[team_num] + except KeyError as e: + raise ModelLookupError( + f'There is no team number {team_num} in this Bout' + ) from e -async def _query_team_or_none( - request: Request, - user: GetUser, - session: AsyncSessionDepends, - team_id: Annotated[int | None, Query(alias='teamId')] = None, -) -> BaseTeam | None: - if team_id is None: - return None - # Query the database for the desired Bout - statement: Select[tuple[BaseTeam]] = select(BaseTeam).where(BaseTeam._id == team_id) - results: Result[tuple[BaseTeam]] = await session.execute(statement) - team: BaseTeam = results.scalar_one() - - return team - - -async def _get_team_or_none( - session: AsyncSessionDepends, - team_id: Annotated[int | None, Body(alias='teamId')] = None, -) -> BaseTeam | None: - if team_id is None: - return None - - # Query the database for the desired Bout - statement: Select[tuple[BaseTeam]] = select(BaseTeam).where(BaseTeam._id == team_id) - results: Result[tuple[BaseTeam]] = await session.execute(statement) - team: BaseTeam = results.scalar_one() - - return team - - -OptionalTeamDepends: TypeAlias = Annotated[ - BaseTeam | None, Depends(_query_team_or_none) -] -GetTeamOrNoneByID: TypeAlias = Annotated[BaseTeam | None, Depends(_get_team_or_none)] +GetTeam: TypeAlias = Annotated[BaseTeam, Depends(_get_team)] diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index e6eecce4..0ad729bf 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -36,7 +36,7 @@ class BaseTeam(BaseSQLModel): roster_uuid: Mapped[int] = mapped_column(ForeignKey('rosters.uuid')) bout_uuid: Mapped[int | None] = mapped_column( - ForeignKey('bouts._id'), nullable=False + ForeignKey('bouts.uuid'), nullable=False ) # TODO: Implement Team colors @@ -83,8 +83,8 @@ class BaseTeam(BaseSQLModel): __tablename__: str = 'teams' __table_args__: tuple[Constraint, ...] = ( - UniqueConstraint('_bout_id', '_roster_id'), - UniqueConstraint('_bout_id', 'num'), + UniqueConstraint('bout_uuid', 'roster_uuid'), + UniqueConstraint('bout_uuid', 'num'), ) __mapper_args__: dict[str, Any] = { 'polymorphic_abstract': True, diff --git a/backend/src/game/timeouts/dependencies.py b/backend/src/game/timeouts/dependencies.py index 898ee25f..21ec4387 100644 --- a/backend/src/game/timeouts/dependencies.py +++ b/backend/src/game/timeouts/dependencies.py @@ -1,35 +1,25 @@ """The FastAPI dependencies methods for Timeouts.""" -from typing import TYPE_CHECKING, Annotated, TypeAlias +from typing import Annotated, TypeAlias -from core import AsyncSessionDepends from core.exceptions import ModelLookupError from fastapi import Depends, Query, Request -from sqlalchemy import select -from sqlalchemy.exc import NoResultFound +from game.bouts.dependencies import GetBout from user import GetUser from .models import BaseTimeout -if TYPE_CHECKING: - from sqlalchemy import Result, Select - async def _get_timeout( request: Request, user: GetUser, - session: AsyncSessionDepends, - timeout_id: Annotated[int, Query(alias='timeoutId')], + bout: GetBout, + num: Annotated[int, Query()], ) -> BaseTimeout: - statement: Select[tuple[BaseTimeout]] = select(BaseTimeout).where( - BaseTimeout._id == timeout_id - ) - results: Result[tuple[BaseTimeout]] = await session.execute(statement) - try: - timeout: BaseTimeout = results.scalar_one() - except NoResultFound as e: - raise ModelLookupError(f'Could not find Timeout with ID {timeout_id}') from e + timeout: BaseTimeout = bout.timeouts[num] + except KeyError as e: + raise ModelLookupError(f'Could not find Timeout {num} in this Bout') from e # Optionally take a snapshot of the Timeout state if request.method != 'GET': @@ -37,4 +27,4 @@ async def _get_timeout( return timeout -GetTimeoutByID: TypeAlias = Annotated[BaseTimeout, Depends(_get_timeout)] +GetTimeout: TypeAlias = Annotated[BaseTimeout, Depends(_get_timeout)] diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index 6474014f..1588b004 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -70,7 +70,7 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): 'polymorphic_abstract': True, 'polymorphic_on': ruleset, } - __table_args__: tuple[Constraint, ...] = (UniqueConstraint('_bout_id', 'num'),) + __table_args__: tuple[Constraint, ...] = (UniqueConstraint('bout_uuid', 'num'),) def __str__(self) -> str: """Return a str representation of this Timeout. diff --git a/backend/src/game/timeouts/router.py b/backend/src/game/timeouts/router.py index 71404d9b..b132455b 100644 --- a/backend/src/game/timeouts/router.py +++ b/backend/src/game/timeouts/router.py @@ -3,9 +3,9 @@ from typing import Annotated, Final, Literal from fastapi import APIRouter, Body -from game.teams.dependencies import GetTeamOrNoneByID +from game.teams.dependencies import GetTeam -from .dependencies import GetTimeoutByID, _get_timeout +from .dependencies import GetTimeout, _get_timeout from .schemas import TimeoutSchema TIMEOUTS_TAG = 'Timeouts' @@ -18,7 +18,7 @@ @router.post('/type', tags=[TIMEOUTS_TAG]) async def set_type( - timeout: GetTimeoutByID, + timeout: GetTimeout, is_review: Annotated[Literal['timeout', 'review'], Body()], ) -> None: """Set the type of the specified Timeout.""" @@ -26,27 +26,25 @@ async def set_type( @router.post('/team', tags=[TIMEOUTS_TAG]) -async def set_team(timeout: GetTimeoutByID, team: GetTeamOrNoneByID) -> None: +async def set_team(timeout: GetTimeout, team: GetTeam) -> None: """Set the calling Team of the specified Timeout.""" timeout.set_team(team) pass @router.post('/retained', tags=[TIMEOUTS_TAG]) -async def set_retained( - timeout: GetTimeoutByID, retained: Annotated[bool, Body()] -) -> None: +async def set_retained(timeout: GetTimeout, retained: Annotated[bool, Body()]) -> None: """Set whether or not the Timeout is retained.""" timeout.set_retained(retained) @router.put('/details', tags=[TIMEOUTS_TAG]) -async def set_details(timeout: GetTimeoutByID, details: Annotated[str, Body()]) -> None: +async def set_details(timeout: GetTimeout, details: Annotated[str, Body()]) -> None: """Add details about the specified Timeout.""" timeout.details = details @router.put('/result', tags=[TIMEOUTS_TAG]) -async def set_result(timeout: GetTimeoutByID, result: Annotated[str, Body()]) -> None: +async def set_result(timeout: GetTimeout, result: Annotated[str, Body()]) -> None: """Add results about the specified Timeout.""" timeout.result = result From b5b0786c5547367387b99a6599622190f8435332 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 20:55:10 -0800 Subject: [PATCH 018/105] switch to UUID based db --- backend/src/game/bouts/dependencies.py | 2 +- backend/src/game/bouts/models.py | 3 +- backend/src/game/bouts/router.py | 23 +++++----- backend/src/game/jams/dependencies.py | 28 +++++------- backend/src/game/jams/models.py | 18 ++++---- backend/src/game/jams/router.py | 29 ++++++------ backend/src/game/rosters/dependencies.py | 11 +++-- backend/src/game/rosters/models.py | 10 ++--- backend/src/game/rulesets/wftda_2025.py | 46 +++++++++---------- backend/src/game/teams/dependencies.py | 54 +++++------------------ backend/src/game/teams/models.py | 11 ++--- backend/src/game/timeouts/dependencies.py | 26 ++++------- backend/src/game/timeouts/models.py | 2 +- backend/src/game/timeouts/router.py | 16 +++---- backend/src/game/trip_events/models.py | 7 +-- 15 files changed, 115 insertions(+), 171 deletions(-) diff --git a/backend/src/game/bouts/dependencies.py b/backend/src/game/bouts/dependencies.py index ad259745..a08a6671 100644 --- a/backend/src/game/bouts/dependencies.py +++ b/backend/src/game/bouts/dependencies.py @@ -37,4 +37,4 @@ async def _get_bout( return bout -BoutDepends: TypeAlias = Annotated[BaseBout, Depends(_get_bout)] +GetBout: TypeAlias = Annotated[BaseBout, Depends(_get_bout)] diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 835bba68..683ebe52 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -36,7 +36,6 @@ class BaseBout(CacheableSQLModel): _clock_uuid: Mapped[UUID] = mapped_column( ForeignKey('clocks.uuid', ondelete='RESTRICT') ) - uuid: Mapped[UUID] = mapped_column(index=True) series_uuid: Mapped[UUID] = mapped_column(ForeignKey('series.uuid')) start_countdown: Mapped[datetime | None] = mapped_column(default=None) @@ -71,7 +70,7 @@ class BaseBout(CacheableSQLModel): back_populates='_bout', cascade=CASCADE_CHILD, lazy='selectin', - order_by=[column('_id')], + order_by=[column('num')], ) __tablename__: str = 'bouts' diff --git a/backend/src/game/bouts/router.py b/backend/src/game/bouts/router.py index ad71d310..90770a2d 100644 --- a/backend/src/game/bouts/router.py +++ b/backend/src/game/bouts/router.py @@ -6,10 +6,9 @@ from core import AsyncSessionDepends from fastapi import APIRouter, Body from game.rulesets.schemas import Ruleset -from game.teams.dependencies import OptionalTeamDepends from sqlalchemy import Result, Select, select -from .dependencies import BoutDepends, _get_bout +from .dependencies import GetBout, _get_bout from .models import BaseBout from .schemas import BoutSchema @@ -31,49 +30,49 @@ async def get_all_bouts(session: AsyncSessionDepends) -> Sequence[BaseBout]: @router.get('/ruleset', response_model=Ruleset) -async def _get_ruleset(bout: BoutDepends) -> Ruleset: +async def _get_ruleset(bout: GetBout) -> Ruleset: return bout.ruleset @router.post('/beginPeriod') -async def begin_period(bout: BoutDepends) -> None: +async def begin_period(bout: GetBout) -> None: """Begin the period of the specified Bout.""" await bout.begin_period(datetime.now()) @router.post('/endPeriod') -async def end_period(bout: BoutDepends) -> None: +async def end_period(bout: GetBout) -> None: """End the period of the specified Bout.""" await bout.end_period(datetime.now()) @router.post('/startJam') -async def start_jam(bout: BoutDepends) -> None: +async def start_jam(bout: GetBout) -> None: """Start the next Jam of the specified Bout.""" await bout.start_jam(datetime.now()) @router.post('/stopJam') -async def stop_jam(bout: BoutDepends) -> None: +async def stop_jam(bout: GetBout) -> None: """Stop the active Jam of the specified Bout.""" await bout.stop_jam(datetime.now()) @router.post('/startTimeout') async def start_timeout( - bout: BoutDepends, - team: OptionalTeamDepends = None, # TODO: dependency should be in Body + bout: GetBout, + team_num: Annotated[int | None, Body(alias='teamNum')] = None, is_review: Annotated[bool, Body(alias='isReview')] = False, ) -> None: """Start a new Timeout in the specified Bout.""" timeout: BaseTimeout = await bout.start_timeout(datetime.now()) timeout.is_review = is_review - if team is not None: - timeout.team = team + if team_num is not None: + timeout.team = bout.teams[team_num] @router.post(path='/stopTimeout') -async def stop_timeout(bout: BoutDepends) -> None: +async def stop_timeout(bout: GetBout) -> None: """Stop the active Timeout in the specified Bout.""" await bout.stop_timeout(datetime.now()) diff --git a/backend/src/game/jams/dependencies.py b/backend/src/game/jams/dependencies.py index f8c267ca..0183afb7 100644 --- a/backend/src/game/jams/dependencies.py +++ b/backend/src/game/jams/dependencies.py @@ -1,34 +1,28 @@ """The FastAPI dependencies methods for Jams.""" -from typing import TYPE_CHECKING, Annotated, TypeAlias +from typing import Annotated, TypeAlias -from core import AsyncSessionDepends from core.exceptions import ModelLookupError from fastapi import Depends, Query, Request -from sqlalchemy import select -from sqlalchemy.exc import NoResultFound +from game.bouts.dependencies import GetBout from user import GetUser from .models import BaseJam -if TYPE_CHECKING: - from sqlalchemy.engine.result import Result - from sqlalchemy.sql.selectable import Select - async def _get_jam( request: Request, user: GetUser, - session: AsyncSessionDepends, - jam_id: Annotated[int, Query(alias='jamId')], + bout: GetBout, + period: Annotated[int, Query()], + num: Annotated[int, Query()], ) -> BaseJam: - statement: Select[tuple[BaseJam]] = select(BaseJam).where(BaseJam._id == jam_id) - results: Result[tuple[BaseJam]] = await session.execute(statement) - try: - jam: BaseJam = results.scalar_one() - except NoResultFound as e: - raise ModelLookupError(f'Could not find Jam with ID {jam_id}') from e + jam: BaseJam = next( + jam for jam in bout.jams if jam.period == period and jam.num == num + ) + except StopIteration as e: + raise ModelLookupError(f'Could not find P{period} J{num} in this Bout') from e if request.method != 'GET': user.stage(jam.get_memento()) @@ -36,4 +30,4 @@ async def _get_jam( return jam -GetJamByID: TypeAlias = Annotated[BaseJam, Depends(_get_jam)] +GetJam: TypeAlias = Annotated[BaseJam, Depends(_get_jam)] diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index 3c873784..e1801f38 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -62,7 +62,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): 'confirm_deleted_rows': False, } __table_args__: tuple[Constraint, ...] = AbstractOneShotModel.__table_args__ + ( - UniqueConstraint('_bout_id', 'num', 'period'), + UniqueConstraint('_bout_uuid', 'num', 'period'), ) def __str__(self) -> str: @@ -143,33 +143,33 @@ def lead_is_declared(self) -> bool: return True return False - async def add_trip(self, team_id: int, timestamp: datetime, passes: int) -> None: + async def add_trip(self, team: BaseTeam, timestamp: datetime, passes: int) -> None: """Add a Jammer trip to the desired Team's TeamJam. Args: - team_id (int): the ID of the desired Team. + team (BaseTeam): the desired Team. timestamp (datetime): the timestamp at which to add the trip. passes (int): the number of passes the Jammer earned. """ ... - async def set_lead(self, team_id: int, timestamp: datetime, lead: bool) -> None: + async def set_lead(self, team: BaseTeam, timestamp: datetime, lead: bool) -> None: """Set the lead Jammer status for the desired Team. Args: - team_id (int): the ID of the desired Team. + team (BaseTeam): the desired Team. timestamp (datetime): the timestamp at which to set lead. lead (bool): True if the Jammer has been declared lead. """ ... - async def set_lost(self, team_id: int, timestamp: datetime, lost: bool) -> None: + async def set_lost(self, team: BaseTeam, timestamp: datetime, lost: bool) -> None: """Set the lead eligibility for the desired Team. Args: - team_id (int): the ID of the desired Team. + team (BaseTeam): the desired Team. timestamp (datetime): the timestamp at which to set lead eligibility. lost (bool): True if the Jammer has lost lead eligibility. @@ -177,12 +177,12 @@ async def set_lost(self, team_id: int, timestamp: datetime, lost: bool) -> None: ... async def set_star_pass( - self, team_id: int, timestamp: datetime, star_pass: bool + self, team: BaseTeam, timestamp: datetime, star_pass: bool ) -> None: """Add a star pass to the desired Team. Args: - team_id (int): the ID of the desired Team. + team (BaseTeam): the desired Team. timestamp (datetime): the timestamp at which to add the star pass. star_pass (bool): True if the star has been successfully passed. diff --git a/backend/src/game/jams/router.py b/backend/src/game/jams/router.py index 8f27b82d..2f777d16 100644 --- a/backend/src/game/jams/router.py +++ b/backend/src/game/jams/router.py @@ -3,9 +3,10 @@ from datetime import datetime from typing import Annotated, Final -from fastapi import APIRouter, Body, Query +from fastapi import APIRouter, Body +from game.teams.dependencies import GetTeam -from .dependencies import GetJamByID, _get_jam +from .dependencies import GetJam, _get_jam from .schemas import JamSchema JAMS_TAG = 'Jams' @@ -16,39 +17,39 @@ @router.post('/addTrip', tags=[JAMS_TAG]) async def add_trip( - jam: GetJamByID, - team_id: Annotated[int, Query(alias='teamId')], + jam: GetJam, + team: GetTeam, passes: Annotated[int, Body()], ) -> None: """Add a Trip for the specified Team of the specified Jam.""" - await jam.add_trip(team_id, datetime.now(), passes) + await jam.add_trip(team, datetime.now(), passes) @router.post('/setLead', tags=[JAMS_TAG]) async def set_lead( - jam: GetJamByID, - team_id: Annotated[int, Query(alias='teamId')], + jam: GetJam, + team: GetTeam, lead: Annotated[bool, Body()], ) -> None: """Set Lead for the specified Team of the specified Jam.""" - await jam.set_lead(team_id, datetime.now(), lead) + await jam.set_lead(team, datetime.now(), lead) @router.post('/setLost', tags=[JAMS_TAG]) async def set_lost( - jam: GetJamByID, - team_id: Annotated[int, Query(alias='teamId')], + jam: GetJam, + team: GetTeam, lost: Annotated[bool, Body()], ) -> None: """Set Lost for the specified Team of the specified Jam.""" - await jam.set_lost(team_id, datetime.now(), lost) + await jam.set_lost(team, datetime.now(), lost) @router.post('/setStarPass', tags=[JAMS_TAG]) async def set_star_pass( - jam: GetJamByID, - team_id: Annotated[int, Query(alias='teamId')], + jam: GetJam, + team: GetTeam, star_pass: Annotated[bool, Body()], ) -> None: """Set a Star Pass for the specified Team of the specified Jam.""" - await jam.set_star_pass(team_id, datetime.now(), star_pass) + await jam.set_star_pass(team, datetime.now(), star_pass) diff --git a/backend/src/game/rosters/dependencies.py b/backend/src/game/rosters/dependencies.py index bcd7c0ab..b092690f 100644 --- a/backend/src/game/rosters/dependencies.py +++ b/backend/src/game/rosters/dependencies.py @@ -5,7 +5,7 @@ from core import AsyncSessionDepends from core.exceptions import ModelLookupError from fastapi import Depends, Query -from sqlalchemy import Result, select +from sqlalchemy import Result, Select, select from sqlalchemy.exc import NoResultFound from .models import Roster @@ -13,16 +13,15 @@ async def _get_roster( session: AsyncSessionDepends, - roster_id: Annotated[int, Query(alias='rosterId')], + uuid: Annotated[int, Query(alias='rosterId')], ) -> Roster: - results: Result[tuple[Roster]] = await session.execute( - select(Roster).where(Roster._id == roster_id) - ) + statement: Select[tuple[Roster]] = select(Roster).where(Roster.uuid == uuid) + results: Result[tuple[Roster]] = await session.execute(statement) try: roster: Roster = results.scalar_one() except NoResultFound as e: - raise ModelLookupError(f'Could not find Roster with ID {roster_id}') from e + raise ModelLookupError(f'Could not find Roster with UUID {uuid}') from e # TODO: handle mementos diff --git a/backend/src/game/rosters/models.py b/backend/src/game/rosters/models.py index 20832dfd..47cf0fa0 100644 --- a/backend/src/game/rosters/models.py +++ b/backend/src/game/rosters/models.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import override -from uuid import UUID, uuid4 +from uuid import UUID # noqa: TC003 from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel from game.models import CacheableSQLModel, CacheKey @@ -14,7 +14,7 @@ class Skater(BaseSQLModel): """Represent a singular Skater in roller derby.""" - _roster_id: Mapped[int] = mapped_column(ForeignKey('rosters._id')) + roster_uuid: Mapped[UUID] = mapped_column(ForeignKey('rosters.uuid')) name: Mapped[str] = mapped_column() pronouns: Mapped[str] = mapped_column() # TODO: Implement pronouns number: Mapped[str] = mapped_column() @@ -22,7 +22,7 @@ class Skater(BaseSQLModel): _roster: Mapped[Roster] = relationship( back_populates='skaters', cascade=CASCADE_OTHER, - foreign_keys=[_roster_id], + foreign_keys=[roster_uuid], lazy='selectin', ) @@ -59,8 +59,6 @@ async def get_roster(self) -> Roster: class Roster(CacheableSQLModel): """Represent a Roster of skaters.""" - uuid: Mapped[UUID] = mapped_column(index=True) - name: Mapped[str] = mapped_column() league: Mapped[str] = mapped_column() mnemonic: Mapped[str] = mapped_column() @@ -94,7 +92,7 @@ def __init__(self, name: str, league: str, mnemonic: str = '') -> None: mnemonic = mnemonic.strip() if mnemonic == '': pass # TODO: Implement team name mnemonic algorithm - super().__init__(uuid=uuid4(), name=name, league=league, mnemonic=mnemonic) + super().__init__(name=name, league=league, mnemonic=mnemonic) @override async def cache_key(self) -> CacheKey: diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index 57955392..a3cf65de 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -231,25 +231,25 @@ class Jam(_WFTDAModel, BaseJam): """A Jam model using the WFTDA 2025 ruleset.""" @override - async def add_trip(self, team_id: int, timestamp: datetime, passes: int) -> None: - team_jam: TeamJam = self.get_team_jam(team_id) + async def add_trip(self, team: BaseTeam, timestamp: datetime, passes: int) -> None: + team_jam: TeamJam = self.get_team_jam(team) - logging.info(f'Adding {passes} passes to Team ID {team_id} in {self}') + logging.info(f'Adding {passes} passes to {team} in {self}') is_initial: bool = len(team_jam.events) == 0 is_overtime: bool = self.period >= Bout.ruleset.num_periods if is_initial: - logging.info(f'This is the initial pass for Team ID {team_id} in {self}') + logging.info(f'This is the initial pass for {team} in {self}') event: TripEvent = TripEvent(timestamp, passes=passes) # Automatically set lead on the first 4-point trip if not self.lead_is_declared() and passes == Bout.ruleset.points_per_trip: - await self.set_lead(team_id, timestamp, True) + await self.set_lead(team, timestamp, True) # Lose eligibility on initial no-pass/no-penalty if len(team_jam.events) == 0 and passes < Bout.ruleset.points_per_trip: - await self.set_lost(team_id, timestamp, True) + await self.set_lost(team, timestamp, True) # Jammer cannot earn points on the initial pass if is_initial and not is_overtime: @@ -258,12 +258,10 @@ async def add_trip(self, team_id: int, timestamp: datetime, passes: int) -> None team_jam.events.append(event) @override - async def set_lead(self, team_id: int, timestamp: datetime, lead: bool) -> None: - team_jam: TeamJam = self.get_team_jam(team_id) + async def set_lead(self, team: BaseTeam, timestamp: datetime, lead: bool) -> None: + team_jam: TeamJam = self.get_team_jam(team) - logging.info( - f'{"Setting" if lead else "Unsetting"} lead for Team ID {team_id} in {self}' - ) + logging.info(f'{"Setting" if lead else "Unsetting"} lead for {team} in {self}') if lead: # Add a new Trip Event in which lead is declared @@ -280,12 +278,10 @@ async def set_lead(self, team_id: int, timestamp: datetime, lead: bool) -> None: team_jam.events.remove(event) @override - async def set_lost(self, team_id: int, timestamp: datetime, lost: bool) -> None: - team_jam: TeamJam = self.get_team_jam(team_id) + async def set_lost(self, team: BaseTeam, timestamp: datetime, lost: bool) -> None: + team_jam: TeamJam = self.get_team_jam(team) - logging.info( - f'{"Setting" if lost else "Unsetting"} lost for Team ID {team_id} in {self}' - ) + logging.info(f'{"Setting" if lost else "Unsetting"} lost for {team} in {self}') if lost: # Add a new Trip Event in which the Jammer has lost eligibility for lead @@ -303,13 +299,12 @@ async def set_lost(self, team_id: int, timestamp: datetime, lost: bool) -> None: @override async def set_star_pass( - self, team_id: int, timestamp: datetime, star_pass: bool + self, team: BaseTeam, timestamp: datetime, star_pass: bool ) -> None: - team_jam: TeamJam = self.get_team_jam(team_id) + team_jam: TeamJam = self.get_team_jam(team) logging.info( - f'{"Setting" if star_pass else "Unsetting"} star pass for Team ID ' - f'{team_id} in {self}' + f'{"Setting" if star_pass else "Unsetting"} star pass {team} in {self}' ) if star_pass: @@ -339,22 +334,21 @@ class Timeout(_WFTDAModel, BaseTimeout): def set_type(self, is_review: bool) -> None: logging.info( f'Setting {self} to {"official review" if is_review else "timeout"} type ' - f'in Bout ID {self._bout_id}' + f'in Bout ID {self.bout_uuid}' ) self.is_review = is_review @override def set_team(self, team: BaseTeam | None) -> None: - if team is not None and team._bout_id != self._bout_id: + if team is not None and team.bout_uuid != self.bout_uuid: raise GameStateError('Team and timeout are not part of the same Bout') if team is None and self.is_review: raise GameRulesError('Official reviews can only be called by teams') logging.info( - f'Setting Timeout ID {self._id} calling team to ' - f'{f"Team ID {team._id}" if team is not None else "officials"} in Bout ID ' - f'{self._bout_id}' + f'Setting {self} calling team to {team or "officials"} in Bout ID ' + f'{self.bout_uuid}' ) self.team = team @@ -364,7 +358,7 @@ def set_team(self, team: BaseTeam | None) -> None: def set_retained(self, retained: bool) -> None: logging.info( f'Setting {self} to {"" if retained else "un"}retained in Bout ID ' - f'{self._bout_id}' + f'{self.bout_uuid}' ) self.retained = retained diff --git a/backend/src/game/teams/dependencies.py b/backend/src/game/teams/dependencies.py index 8ef5b51e..13536fbc 100644 --- a/backend/src/game/teams/dependencies.py +++ b/backend/src/game/teams/dependencies.py @@ -1,51 +1,21 @@ """The FastAPI dependencies methods for Teams.""" -from typing import TYPE_CHECKING, Annotated, TypeAlias +from typing import Annotated, TypeAlias -from core import AsyncSessionDepends -from fastapi import Body, Depends, Query, Request -from sqlalchemy import select -from user import GetUser +from core.exceptions import ModelLookupError +from fastapi import Depends, Query +from game.bouts.dependencies import GetBout from .models import BaseTeam -if TYPE_CHECKING: - from sqlalchemy import Result, Select +async def _get_team(bout: GetBout, team_num: Annotated[int, Query()]) -> BaseTeam: + try: + return bout.teams[team_num] + except KeyError as e: + raise ModelLookupError( + f'There is no team number {team_num} in this Bout' + ) from e -async def _query_team_or_none( - request: Request, - user: GetUser, - session: AsyncSessionDepends, - team_id: Annotated[int | None, Query(alias='teamId')] = None, -) -> BaseTeam | None: - if team_id is None: - return None - # Query the database for the desired Bout - statement: Select[tuple[BaseTeam]] = select(BaseTeam).where(BaseTeam._id == team_id) - results: Result[tuple[BaseTeam]] = await session.execute(statement) - team: BaseTeam = results.scalar_one() - - return team - - -async def _get_team_or_none( - session: AsyncSessionDepends, - team_id: Annotated[int | None, Body(alias='teamId')] = None, -) -> BaseTeam | None: - if team_id is None: - return None - - # Query the database for the desired Bout - statement: Select[tuple[BaseTeam]] = select(BaseTeam).where(BaseTeam._id == team_id) - results: Result[tuple[BaseTeam]] = await session.execute(statement) - team: BaseTeam = results.scalar_one() - - return team - - -OptionalTeamDepends: TypeAlias = Annotated[ - BaseTeam | None, Depends(_query_team_or_none) -] -GetTeamOrNoneByID: TypeAlias = Annotated[BaseTeam | None, Depends(_get_team_or_none)] +GetTeam: TypeAlias = Annotated[BaseTeam, Depends(_get_team)] diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index e6eecce4..bd2ec35f 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, Final, override +from uuid import UUID # noqa: TC003 from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel from game.bouts.models import BaseBout @@ -34,9 +35,9 @@ class BaseTeam(BaseSQLModel): data like a team's score offset. """ - roster_uuid: Mapped[int] = mapped_column(ForeignKey('rosters.uuid')) - bout_uuid: Mapped[int | None] = mapped_column( - ForeignKey('bouts._id'), nullable=False + roster_uuid: Mapped[UUID] = mapped_column(ForeignKey('rosters.uuid')) + bout_uuid: Mapped[UUID | None] = mapped_column( + ForeignKey('bouts.uuid'), nullable=False ) # TODO: Implement Team colors @@ -83,8 +84,8 @@ class BaseTeam(BaseSQLModel): __tablename__: str = 'teams' __table_args__: tuple[Constraint, ...] = ( - UniqueConstraint('_bout_id', '_roster_id'), - UniqueConstraint('_bout_id', 'num'), + UniqueConstraint('bout_uuid', 'roster_uuid'), + UniqueConstraint('bout_uuid', 'num'), ) __mapper_args__: dict[str, Any] = { 'polymorphic_abstract': True, diff --git a/backend/src/game/timeouts/dependencies.py b/backend/src/game/timeouts/dependencies.py index 898ee25f..21ec4387 100644 --- a/backend/src/game/timeouts/dependencies.py +++ b/backend/src/game/timeouts/dependencies.py @@ -1,35 +1,25 @@ """The FastAPI dependencies methods for Timeouts.""" -from typing import TYPE_CHECKING, Annotated, TypeAlias +from typing import Annotated, TypeAlias -from core import AsyncSessionDepends from core.exceptions import ModelLookupError from fastapi import Depends, Query, Request -from sqlalchemy import select -from sqlalchemy.exc import NoResultFound +from game.bouts.dependencies import GetBout from user import GetUser from .models import BaseTimeout -if TYPE_CHECKING: - from sqlalchemy import Result, Select - async def _get_timeout( request: Request, user: GetUser, - session: AsyncSessionDepends, - timeout_id: Annotated[int, Query(alias='timeoutId')], + bout: GetBout, + num: Annotated[int, Query()], ) -> BaseTimeout: - statement: Select[tuple[BaseTimeout]] = select(BaseTimeout).where( - BaseTimeout._id == timeout_id - ) - results: Result[tuple[BaseTimeout]] = await session.execute(statement) - try: - timeout: BaseTimeout = results.scalar_one() - except NoResultFound as e: - raise ModelLookupError(f'Could not find Timeout with ID {timeout_id}') from e + timeout: BaseTimeout = bout.timeouts[num] + except KeyError as e: + raise ModelLookupError(f'Could not find Timeout {num} in this Bout') from e # Optionally take a snapshot of the Timeout state if request.method != 'GET': @@ -37,4 +27,4 @@ async def _get_timeout( return timeout -GetTimeoutByID: TypeAlias = Annotated[BaseTimeout, Depends(_get_timeout)] +GetTimeout: TypeAlias = Annotated[BaseTimeout, Depends(_get_timeout)] diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index 6474014f..1588b004 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -70,7 +70,7 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): 'polymorphic_abstract': True, 'polymorphic_on': ruleset, } - __table_args__: tuple[Constraint, ...] = (UniqueConstraint('_bout_id', 'num'),) + __table_args__: tuple[Constraint, ...] = (UniqueConstraint('bout_uuid', 'num'),) def __str__(self) -> str: """Return a str representation of this Timeout. diff --git a/backend/src/game/timeouts/router.py b/backend/src/game/timeouts/router.py index 71404d9b..b132455b 100644 --- a/backend/src/game/timeouts/router.py +++ b/backend/src/game/timeouts/router.py @@ -3,9 +3,9 @@ from typing import Annotated, Final, Literal from fastapi import APIRouter, Body -from game.teams.dependencies import GetTeamOrNoneByID +from game.teams.dependencies import GetTeam -from .dependencies import GetTimeoutByID, _get_timeout +from .dependencies import GetTimeout, _get_timeout from .schemas import TimeoutSchema TIMEOUTS_TAG = 'Timeouts' @@ -18,7 +18,7 @@ @router.post('/type', tags=[TIMEOUTS_TAG]) async def set_type( - timeout: GetTimeoutByID, + timeout: GetTimeout, is_review: Annotated[Literal['timeout', 'review'], Body()], ) -> None: """Set the type of the specified Timeout.""" @@ -26,27 +26,25 @@ async def set_type( @router.post('/team', tags=[TIMEOUTS_TAG]) -async def set_team(timeout: GetTimeoutByID, team: GetTeamOrNoneByID) -> None: +async def set_team(timeout: GetTimeout, team: GetTeam) -> None: """Set the calling Team of the specified Timeout.""" timeout.set_team(team) pass @router.post('/retained', tags=[TIMEOUTS_TAG]) -async def set_retained( - timeout: GetTimeoutByID, retained: Annotated[bool, Body()] -) -> None: +async def set_retained(timeout: GetTimeout, retained: Annotated[bool, Body()]) -> None: """Set whether or not the Timeout is retained.""" timeout.set_retained(retained) @router.put('/details', tags=[TIMEOUTS_TAG]) -async def set_details(timeout: GetTimeoutByID, details: Annotated[str, Body()]) -> None: +async def set_details(timeout: GetTimeout, details: Annotated[str, Body()]) -> None: """Add details about the specified Timeout.""" timeout.details = details @router.put('/result', tags=[TIMEOUTS_TAG]) -async def set_result(timeout: GetTimeoutByID, result: Annotated[str, Body()]) -> None: +async def set_result(timeout: GetTimeout, result: Annotated[str, Body()]) -> None: """Add results about the specified Timeout.""" timeout.result = result diff --git a/backend/src/game/trip_events/models.py b/backend/src/game/trip_events/models.py index 2402deb1..3c26d104 100644 --- a/backend/src/game/trip_events/models.py +++ b/backend/src/game/trip_events/models.py @@ -4,6 +4,7 @@ from datetime import datetime # noqa: TC003 from typing import TYPE_CHECKING, override +from uuid import UUID # noqa: TC003 from core import CASCADE_OTHER, BaseSQLModel from sqlalchemy import CheckConstraint, Constraint, ForeignKey @@ -26,8 +27,8 @@ class TripEvent(BaseSQLModel): eligibility. """ - _team_jam_id: Mapped[int | None] = mapped_column( - ForeignKey('team_jams._id'), nullable=False + _team_jam_uuid: Mapped[UUID | None] = mapped_column( + ForeignKey('team_jams.uuid'), nullable=False ) timestamp: Mapped[datetime] = mapped_column() @@ -37,7 +38,7 @@ class TripEvent(BaseSQLModel): star_pass: Mapped[bool] = mapped_column(default=False) _team_jam: Mapped[TeamJam | None] = relationship( - back_populates='events', cascade=CASCADE_OTHER, foreign_keys=[_team_jam_id] + back_populates='events', cascade=CASCADE_OTHER, foreign_keys=[_team_jam_uuid] ) __tablename__: str = 'trip_events' From 7fbb053b34c8b526085b97e9caa3f9982a63cd59 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 21:04:08 -0800 Subject: [PATCH 019/105] log initially created bout --- backend/src/main.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/src/main.py b/backend/src/main.py index de3afd2e..de987400 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -80,16 +80,15 @@ async def lifespan(app: FastAPI): results: Result[tuple[wftda_2025.Bout]] = await session.execute(statement) if results.scalar_one_or_none() is None: logging.info('Instantiating the initial Bout model') - series: Series = Series() - series.bouts.append( - wftda_2025.Bout( - Roster('Home', 'Default League'), - Roster('Away', 'Default League'), - ) + bout: wftda_2025.Bout = wftda_2025.Bout( + Roster('Home', 'Default League'), + Roster('Away', 'Default League'), ) + series: Series = Series() + series.bouts.append(bout) session.add(series) await session.commit() - logging.debug('The Bout model was inserted into the database') + logging.debug(f'{bout} was inserted into the database') else: logging.debug('Model data was found in the database') From c7ea2b1e400bfbdacd6153031f45a01ddee615ff Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 21:06:51 -0800 Subject: [PATCH 020/105] rename endpoint args --- backend/src/game/bouts/dependencies.py | 9 +++++---- backend/src/game/rosters/dependencies.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/src/game/bouts/dependencies.py b/backend/src/game/bouts/dependencies.py index a08a6671..f523adf0 100644 --- a/backend/src/game/bouts/dependencies.py +++ b/backend/src/game/bouts/dependencies.py @@ -20,16 +20,17 @@ async def _get_bout( request: Request, user: GetUser, session: AsyncSessionDepends, - uuid: Annotated[UUID, Query()], + bout_uuid: Annotated[UUID, Query(alias='boutUuid')], ) -> BaseBout: - # Query the database for the desired Bout - statement: Select[tuple[BaseBout]] = select(BaseBout).where(BaseBout.uuid == uuid) + statement: Select[tuple[BaseBout]] = select(BaseBout).where( + BaseBout.uuid == bout_uuid + ) results: Result[tuple[BaseBout]] = await session.execute(statement) try: bout: BaseBout = results.scalar_one() except NoResultFound as e: - raise ModelLookupError(f'Could not find Bout with UUID {uuid}') from e + raise ModelLookupError(f'Could not find Bout with UUID {bout_uuid}') from e # Optionally take a snapshot of the Bout state and return the Bout if request.method != 'GET': diff --git a/backend/src/game/rosters/dependencies.py b/backend/src/game/rosters/dependencies.py index b092690f..5207a27f 100644 --- a/backend/src/game/rosters/dependencies.py +++ b/backend/src/game/rosters/dependencies.py @@ -13,15 +13,15 @@ async def _get_roster( session: AsyncSessionDepends, - uuid: Annotated[int, Query(alias='rosterId')], + roster_uuid: Annotated[int, Query(alias='rosterUuid')], ) -> Roster: - statement: Select[tuple[Roster]] = select(Roster).where(Roster.uuid == uuid) + statement: Select[tuple[Roster]] = select(Roster).where(Roster.uuid == roster_uuid) results: Result[tuple[Roster]] = await session.execute(statement) try: roster: Roster = results.scalar_one() except NoResultFound as e: - raise ModelLookupError(f'Could not find Roster with UUID {uuid}') from e + raise ModelLookupError(f'Could not find Roster with UUID {roster_uuid}') from e # TODO: handle mementos From 0b40ae2c535a0c15b76c2cca0d163f4c796114cb Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 21:45:21 -0800 Subject: [PATCH 021/105] update schemas --- backend/src/game/jams/schemas.py | 3 ++- backend/src/game/team_jams/schemas.py | 2 +- backend/src/game/timeouts/schemas.py | 21 +++++++++++++++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/src/game/jams/schemas.py b/backend/src/game/jams/schemas.py index c5278c08..99bbcf34 100644 --- a/backend/src/game/jams/schemas.py +++ b/backend/src/game/jams/schemas.py @@ -1,6 +1,7 @@ """Pydantic Jam schemas.""" from datetime import datetime +from uuid import UUID from core import ServerSchema from game.jams.models import StopReasonStr @@ -10,7 +11,7 @@ class JamSchema(ServerSchema): """Represent a Jam as a JSON schema.""" - # FIXME: add Bout UUID + bout_uuid: UUID period: int num: int diff --git a/backend/src/game/team_jams/schemas.py b/backend/src/game/team_jams/schemas.py index 5618189a..947a63e0 100644 --- a/backend/src/game/team_jams/schemas.py +++ b/backend/src/game/team_jams/schemas.py @@ -7,5 +7,5 @@ class TeamJamSchema(ServerSchema): """Represent a TeamJam as a JSON schema.""" - # FIXME: figure out how to refer back to the team that owns this team-jam + team_num: int events: list[TripEventSchema] diff --git a/backend/src/game/timeouts/schemas.py b/backend/src/game/timeouts/schemas.py index c97a7b8e..e67ac007 100644 --- a/backend/src/game/timeouts/schemas.py +++ b/backend/src/game/timeouts/schemas.py @@ -2,17 +2,20 @@ from datetime import datetime, timedelta # noqa: TC003 from typing import Annotated +from uuid import UUID from core import ServerSchema, timedelta_serializer +from game.teams.schemas import TeamSchema +from pydantic import Field, computed_field class TimeoutSchema(ServerSchema): """Represent a Timeout as a JSON schema.""" - # FIXME: add Bout UUID + bout_uuid: UUID num: int - # team_id: int | None # FIXME: add a way to refer to team + team: TeamSchema | None = Field(exclude=True) # FIXME: add period num and jam num, if any start_timestamp: datetime | None @@ -24,3 +27,17 @@ class TimeoutSchema(ServerSchema): details: str result: str retained: bool + + @computed_field + @property + def team_num(self) -> int | None: + """Get the Team number of the Team that called this Timeout, if any. + + Returns: + int | None: the Team number of the calling Team or None if not yet + determined. + + """ + if self.team is None: + return None + return self.team.num From 854857b2913712febf9eb3eada4c3e5560963276 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 21:55:44 -0800 Subject: [PATCH 022/105] add roster uuid --- backend/src/game/teams/schemas.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/game/teams/schemas.py b/backend/src/game/teams/schemas.py index a1e07cd2..234cf1ce 100644 --- a/backend/src/game/teams/schemas.py +++ b/backend/src/game/teams/schemas.py @@ -2,13 +2,15 @@ from __future__ import annotations +from uuid import UUID # noqa: TC003 + from core import ServerSchema class TeamSchema(ServerSchema): """Represent a Team as a JSON schema.""" - # FIXME: add Roster UUID + roster_uuid: UUID num: int bout_score: int jam_score: int From af70e3bef36faa87209c3cc0fc4cb9528dfe688d Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 22:16:25 -0800 Subject: [PATCH 023/105] show team num in team_jam --- backend/src/game/jams/models.py | 12 ++++++------ backend/src/game/team_jams/models.py | 20 ++++++++++++-------- backend/src/game/teams/models.py | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index e1801f38..e98f0f6a 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -30,7 +30,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): """An abstract Jam without any associated ruleset.""" - _bout_uuid: Mapped[UUID | None] = mapped_column( + bout_uuid: Mapped[UUID | None] = mapped_column( ForeignKey('bouts.uuid'), nullable=False ) @@ -41,7 +41,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): _bout: Mapped[BaseBout | None] = relationship( back_populates='jams', cascade=CASCADE_OTHER, - foreign_keys=[_bout_uuid], + foreign_keys=[bout_uuid], ) team_jams: Mapped[list[TeamJam]] = relationship( back_populates='_jam', @@ -51,7 +51,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): _ruleset: MappedSQLExpression[str] = column_property( select(BaseBout.ruleset_name) - .where(BaseBout.uuid == _bout_uuid) + .where(BaseBout.uuid == bout_uuid) .scalar_subquery() ) @@ -62,7 +62,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): 'confirm_deleted_rows': False, } __table_args__: tuple[Constraint, ...] = AbstractOneShotModel.__table_args__ + ( - UniqueConstraint('_bout_uuid', 'num', 'period'), + UniqueConstraint('bout_uuid', 'num', 'period'), ) def __str__(self) -> str: @@ -72,7 +72,7 @@ def __str__(self) -> str: str: a str representation of this Jam. """ - return f'[Bout ID: {self._bout_uuid}, P{self.period} J{self.num}]' + return f'[Bout ID: {self.bout_uuid}, P{self.period} J{self.num}]' def __init__(self, period_num: int, jam_num: int, *team_jams: TeamJam) -> None: """Initialize a Jam. @@ -87,7 +87,7 @@ def __init__(self, period_num: int, jam_num: int, *team_jams: TeamJam) -> None: @override async def cache_key(self) -> CacheKey: - return (self.__tablename__, self._bout_uuid, self.period, self.num) + return (self.__tablename__, self.bout_uuid, self.period, self.num) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: diff --git a/backend/src/game/team_jams/models.py b/backend/src/game/team_jams/models.py index 387f6e89..a789df32 100644 --- a/backend/src/game/team_jams/models.py +++ b/backend/src/game/team_jams/models.py @@ -8,7 +8,7 @@ from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel from game.jams.models import BaseJam from game.trip_events.models import TripEvent -from sqlalchemy import ForeignKey, select +from sqlalchemy import ForeignKey, column, select, table from sqlalchemy.orm import ( Mapped, MappedSQLExpression, @@ -33,12 +33,10 @@ class TeamJam(BaseSQLModel): """ - team_uuid: Mapped[UUID | None] = mapped_column( - ForeignKey('teams.uuid'), nullable=False - ) - jam_uuid: Mapped[UUID | None] = mapped_column( + _jam_uuid: Mapped[UUID | None] = mapped_column( ForeignKey('jams.uuid'), nullable=False ) + team_uuid: Mapped[UUID] = mapped_column(ForeignKey('teams.uuid')) _team: Mapped[BaseTeam | None] = relationship( back_populates='team_jams', @@ -48,7 +46,8 @@ class TeamJam(BaseSQLModel): _jam: Mapped[BaseJam] = relationship( back_populates='team_jams', cascade=CASCADE_OTHER, - foreign_keys=[jam_uuid], + foreign_keys=[_jam_uuid], + lazy='selectin', ) events: Mapped[list[TripEvent]] = relationship( back_populates='_team_jam', @@ -58,10 +57,15 @@ class TeamJam(BaseSQLModel): ) jam_num: MappedSQLExpression[int] = column_property( - select(BaseJam.num).where(BaseJam.uuid == jam_uuid).scalar_subquery() + select(BaseJam.num).where(BaseJam.uuid == _jam_uuid).scalar_subquery() ) period_num: MappedSQLExpression[int] = column_property( - select(BaseJam.period).where(BaseJam.uuid == jam_uuid).scalar_subquery() + select(BaseJam.period).where(BaseJam.uuid == _jam_uuid).scalar_subquery() + ) + team_num: MappedSQLExpression[int] = column_property( + select(table('teams', column('num'))) + .where(column('uuid') == team_uuid) + .scalar_subquery() ) __tablename__: str = 'team_jams' diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index bd2ec35f..a8a7f775 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -167,7 +167,7 @@ def jam_score(self) -> int: """ active_team_jam: TeamJam | None = next( - (tj for tj in self.team_jams if tj.jam_uuid == self._active_jam_uuid), None + (tj for tj in self.team_jams if tj._jam_uuid == self._active_jam_uuid), None ) if active_team_jam is None: return 0 From be63aacca5446a2f2b2c6065523ec54665651b3d Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 22:21:43 -0800 Subject: [PATCH 024/105] make jam relationship public --- backend/src/game/jams/models.py | 2 +- backend/src/game/team_jams/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index e98f0f6a..802e2c81 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -44,7 +44,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): foreign_keys=[bout_uuid], ) team_jams: Mapped[list[TeamJam]] = relationship( - back_populates='_jam', + back_populates='jam', cascade=CASCADE_CHILD, lazy='selectin', ) diff --git a/backend/src/game/team_jams/models.py b/backend/src/game/team_jams/models.py index a789df32..4285d1c1 100644 --- a/backend/src/game/team_jams/models.py +++ b/backend/src/game/team_jams/models.py @@ -43,7 +43,7 @@ class TeamJam(BaseSQLModel): cascade=CASCADE_OTHER, foreign_keys=[team_uuid], ) - _jam: Mapped[BaseJam] = relationship( + jam: Mapped[BaseJam] = relationship( back_populates='team_jams', cascade=CASCADE_OTHER, foreign_keys=[_jam_uuid], From 375feca046c72788a2f5c55dfb9f17f0ea3737c6 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 22:40:48 -0800 Subject: [PATCH 025/105] add more fields --- backend/src/game/rosters/schemas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/game/rosters/schemas.py b/backend/src/game/rosters/schemas.py index dbaa6422..fb8755f0 100644 --- a/backend/src/game/rosters/schemas.py +++ b/backend/src/game/rosters/schemas.py @@ -10,3 +10,5 @@ class RosterSchema(ServerSchema): uuid: UUID name: str + league: str + mnemonic: str From a458a9b64e816c8c731c2f60a0d29e88af219d40 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 22:46:57 -0800 Subject: [PATCH 026/105] rename dependency injection --- backend/src/core/__init__.py | 4 ++-- backend/src/core/dependencies.py | 2 +- backend/src/game/bouts/dependencies.py | 4 ++-- backend/src/game/bouts/router.py | 5 +++-- backend/src/game/rosters/dependencies.py | 4 ++-- backend/src/game/series/dependencies.py | 4 ++-- backend/src/game/series/router.py | 4 ++-- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/backend/src/core/__init__.py b/backend/src/core/__init__.py index a2a2896b..83aab73e 100644 --- a/backend/src/core/__init__.py +++ b/backend/src/core/__init__.py @@ -4,7 +4,7 @@ """ from .database import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel, DatabaseEngine -from .dependencies import AsyncSessionDepends, EngineFactory +from .dependencies import EngineFactory, GetAsyncSession from .protocols import Memento from .router import api_router, assets, pages_router from .schemas import APIResponseClass, ClientSchema, ServerSchema @@ -21,7 +21,6 @@ 'api_router', 'APIResponseClass', 'assets', - 'AsyncSessionDepends', 'BaseSQLModel', 'CASCADE_CHILD', 'CASCADE_OTHER', @@ -33,6 +32,7 @@ 'get_default_route', 'get_resource_path', 'get_server', + 'GetAsyncSession', 'Memento', 'pages_router', 'ServerSchema', diff --git a/backend/src/core/dependencies.py b/backend/src/core/dependencies.py index 1f309c49..08c4b1fc 100644 --- a/backend/src/core/dependencies.py +++ b/backend/src/core/dependencies.py @@ -78,7 +78,7 @@ async def yield_async_session(cls) -> AsyncGenerator[AsyncSession, None]: await session.commit() # Automatically commit after each session -AsyncSessionDepends: TypeAlias = Annotated[ +GetAsyncSession: TypeAlias = Annotated[ AsyncSession, Depends(EngineFactory.yield_async_session), ] diff --git a/backend/src/game/bouts/dependencies.py b/backend/src/game/bouts/dependencies.py index f523adf0..638a4dc1 100644 --- a/backend/src/game/bouts/dependencies.py +++ b/backend/src/game/bouts/dependencies.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated, TypeAlias from uuid import UUID -from core import AsyncSessionDepends +from core import GetAsyncSession from core.exceptions import ModelLookupError from fastapi import Depends, Query, Request from sqlalchemy import select @@ -19,7 +19,7 @@ async def _get_bout( request: Request, user: GetUser, - session: AsyncSessionDepends, + session: GetAsyncSession, bout_uuid: Annotated[UUID, Query(alias='boutUuid')], ) -> BaseBout: statement: Select[tuple[BaseBout]] = select(BaseBout).where( diff --git a/backend/src/game/bouts/router.py b/backend/src/game/bouts/router.py index 90770a2d..c0e07ba1 100644 --- a/backend/src/game/bouts/router.py +++ b/backend/src/game/bouts/router.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import TYPE_CHECKING, Annotated, Final, Sequence -from core import AsyncSessionDepends +from core import GetAsyncSession from fastapi import APIRouter, Body from game.rulesets.schemas import Ruleset from sqlalchemy import Result, Select, select @@ -22,7 +22,8 @@ @router.get('/allBouts', response_model=list[BoutSchema]) -async def get_all_bouts(session: AsyncSessionDepends) -> Sequence[BaseBout]: +async def get_all_bouts(session: GetAsyncSession) -> Sequence[BaseBout]: + """Get all the Bouts in the database.""" statement: Select[tuple[BaseBout]] = select(BaseBout) results: Result[tuple[BaseBout]] = await session.execute(statement) diff --git a/backend/src/game/rosters/dependencies.py b/backend/src/game/rosters/dependencies.py index 5207a27f..410b1d3f 100644 --- a/backend/src/game/rosters/dependencies.py +++ b/backend/src/game/rosters/dependencies.py @@ -2,7 +2,7 @@ from typing import Annotated, TypeAlias -from core import AsyncSessionDepends +from core import GetAsyncSession from core.exceptions import ModelLookupError from fastapi import Depends, Query from sqlalchemy import Result, Select, select @@ -12,7 +12,7 @@ async def _get_roster( - session: AsyncSessionDepends, + session: GetAsyncSession, roster_uuid: Annotated[int, Query(alias='rosterUuid')], ) -> Roster: statement: Select[tuple[Roster]] = select(Roster).where(Roster.uuid == roster_uuid) diff --git a/backend/src/game/series/dependencies.py b/backend/src/game/series/dependencies.py index 40f036c9..8ee2560f 100644 --- a/backend/src/game/series/dependencies.py +++ b/backend/src/game/series/dependencies.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Sequence -from core import AsyncSessionDepends +from core import GetAsyncSession from sqlalchemy import select from .models import Series @@ -12,7 +12,7 @@ from sqlalchemy.sql.selectable import Select -async def _get_all_series(session: AsyncSessionDepends) -> Sequence[Series]: +async def _get_all_series(session: GetAsyncSession) -> Sequence[Series]: statement: Select[tuple[Series]] = select(Series) results: Result[tuple[Series]] = await session.execute(statement) diff --git a/backend/src/game/series/router.py b/backend/src/game/series/router.py index bb207f95..e8460e32 100644 --- a/backend/src/game/series/router.py +++ b/backend/src/game/series/router.py @@ -2,7 +2,7 @@ from typing import Final, Sequence -from core import AsyncSessionDepends +from core import GetAsyncSession from fastapi import APIRouter from sqlalchemy import Result, Select, select @@ -17,7 +17,7 @@ @router.get('/allSeries', response_model=list[SeriesSchema]) -async def get_all_series(session: AsyncSessionDepends) -> Sequence[Series]: +async def get_all_series(session: GetAsyncSession) -> Sequence[Series]: """Get all Series in the database.""" statement: Select[tuple[Series]] = select(Series) results: Result[tuple[Series]] = await session.execute(statement) From 9a02ff37322b247a80e44cf38c86c8f24889a707 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 22:54:56 -0800 Subject: [PATCH 027/105] fix series dependency --- backend/src/game/series/dependencies.py | 29 ++++++++++++++++++++----- backend/src/game/series/router.py | 6 ++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/backend/src/game/series/dependencies.py b/backend/src/game/series/dependencies.py index 8ee2560f..0dcccb88 100644 --- a/backend/src/game/series/dependencies.py +++ b/backend/src/game/series/dependencies.py @@ -1,9 +1,14 @@ """The FastAPI dependencies methods for Series.""" -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING, Annotated, TypeAlias +from uuid import UUID from core import GetAsyncSession +from core.exceptions import ModelLookupError +from fastapi import Depends, Query, Request from sqlalchemy import select +from sqlalchemy.exc import NoResultFound +from user import GetUser from .models import Series @@ -12,10 +17,24 @@ from sqlalchemy.sql.selectable import Select -async def _get_all_series(session: GetAsyncSession) -> Sequence[Series]: - statement: Select[tuple[Series]] = select(Series) +async def _get_series( + request: Request, + user: GetUser, + session: GetAsyncSession, + series_uuid: Annotated[UUID, Query(alias='seriesUuid')], +) -> Series: + statement: Select[tuple[Series]] = select(Series).where(Series.uuid == series_uuid) results: Result[tuple[Series]] = await session.execute(statement) - # TODO: figure out how to handle series dependencies + try: + series: Series = results.scalar_one() + except NoResultFound as e: + raise ModelLookupError(f'Could not find Series ({series_uuid=})') from e - return results.scalars().all() + # Optionally take a snapshot of the Bout state and return the Bout + if request.method != 'GET': + user.stage(series.get_memento()) + return series + + +GetSeries: TypeAlias = Annotated[Series, Depends(_get_series)] diff --git a/backend/src/game/series/router.py b/backend/src/game/series/router.py index e8460e32..f9b62d2d 100644 --- a/backend/src/game/series/router.py +++ b/backend/src/game/series/router.py @@ -6,19 +6,19 @@ from fastapi import APIRouter from sqlalchemy import Result, Select, select -from .dependencies import _get_all_series +from .dependencies import _get_series from .models import Series from .schemas import SeriesSchema SERIES_TAG = 'Series' router: Final[APIRouter] = APIRouter(prefix='/series', tags=[SERIES_TAG]) -router.add_api_route('', _get_all_series, response_model=list[SeriesSchema]) +router.add_api_route('', _get_series, response_model=SeriesSchema) @router.get('/allSeries', response_model=list[SeriesSchema]) async def get_all_series(session: GetAsyncSession) -> Sequence[Series]: - """Get all Series in the database.""" + """Get all the Series in the database.""" statement: Select[tuple[Series]] = select(Series) results: Result[tuple[Series]] = await session.execute(statement) From 995dd4abe40c40f88a083b47baa0dd0d98e2486a Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 23:02:21 -0800 Subject: [PATCH 028/105] standardize error messages --- backend/src/game/bouts/dependencies.py | 2 +- backend/src/game/jams/dependencies.py | 2 +- backend/src/game/rosters/dependencies.py | 2 +- backend/src/game/teams/dependencies.py | 4 +--- backend/src/game/timeouts/dependencies.py | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/src/game/bouts/dependencies.py b/backend/src/game/bouts/dependencies.py index 638a4dc1..aedebb48 100644 --- a/backend/src/game/bouts/dependencies.py +++ b/backend/src/game/bouts/dependencies.py @@ -30,7 +30,7 @@ async def _get_bout( try: bout: BaseBout = results.scalar_one() except NoResultFound as e: - raise ModelLookupError(f'Could not find Bout with UUID {bout_uuid}') from e + raise ModelLookupError(f'Could not find Bout ({bout_uuid=})') from e # Optionally take a snapshot of the Bout state and return the Bout if request.method != 'GET': diff --git a/backend/src/game/jams/dependencies.py b/backend/src/game/jams/dependencies.py index 0183afb7..b606024f 100644 --- a/backend/src/game/jams/dependencies.py +++ b/backend/src/game/jams/dependencies.py @@ -22,7 +22,7 @@ async def _get_jam( jam for jam in bout.jams if jam.period == period and jam.num == num ) except StopIteration as e: - raise ModelLookupError(f'Could not find P{period} J{num} in this Bout') from e + raise ModelLookupError(f'Could not find Jam ({bout=} {period=} {num=})') from e if request.method != 'GET': user.stage(jam.get_memento()) diff --git a/backend/src/game/rosters/dependencies.py b/backend/src/game/rosters/dependencies.py index 410b1d3f..fae93872 100644 --- a/backend/src/game/rosters/dependencies.py +++ b/backend/src/game/rosters/dependencies.py @@ -21,7 +21,7 @@ async def _get_roster( try: roster: Roster = results.scalar_one() except NoResultFound as e: - raise ModelLookupError(f'Could not find Roster with UUID {roster_uuid}') from e + raise ModelLookupError(f'Could not find Roster ({roster_uuid=})') from e # TODO: handle mementos diff --git a/backend/src/game/teams/dependencies.py b/backend/src/game/teams/dependencies.py index 13536fbc..63980f23 100644 --- a/backend/src/game/teams/dependencies.py +++ b/backend/src/game/teams/dependencies.py @@ -13,9 +13,7 @@ async def _get_team(bout: GetBout, team_num: Annotated[int, Query()]) -> BaseTea try: return bout.teams[team_num] except KeyError as e: - raise ModelLookupError( - f'There is no team number {team_num} in this Bout' - ) from e + raise ModelLookupError(f'Could not find Team ({bout=} {team_num=})') from e GetTeam: TypeAlias = Annotated[BaseTeam, Depends(_get_team)] diff --git a/backend/src/game/timeouts/dependencies.py b/backend/src/game/timeouts/dependencies.py index 21ec4387..c6034c76 100644 --- a/backend/src/game/timeouts/dependencies.py +++ b/backend/src/game/timeouts/dependencies.py @@ -19,7 +19,7 @@ async def _get_timeout( try: timeout: BaseTimeout = bout.timeouts[num] except KeyError as e: - raise ModelLookupError(f'Could not find Timeout {num} in this Bout') from e + raise ModelLookupError(f'Could not find Timeout ({bout=} {num=})') from e # Optionally take a snapshot of the Timeout state if request.method != 'GET': From cf7e4189a51dd49a41c4d7fb58225158d3d70174 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Thu, 22 Jan 2026 23:03:34 -0800 Subject: [PATCH 029/105] update ruff --- uv.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/uv.lock b/uv.lock index 9dbbf3f7..8648e68e 100644 --- a/uv.lock +++ b/uv.lock @@ -806,28 +806,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, - { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, - { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, - { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] From 52d1be509b031b1a95d15865556402fd14310a75 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Fri, 23 Jan 2026 19:40:51 -0800 Subject: [PATCH 030/105] upgrade dependencies --- uv.lock | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/uv.lock b/uv.lock index 8648e68e..6c2fe18f 100644 --- a/uv.lock +++ b/uv.lock @@ -239,18 +239,19 @@ wheels = [ [[package]] name = "greenlet" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, - { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, - { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, - { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, + { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, ] [[package]] From c1fceedbe8895380ba85db397818dc055bde1a77 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Fri, 23 Jan 2026 21:45:41 -0800 Subject: [PATCH 031/105] rename module to better reflect main object --- backend/src/game/__init__.py | 6 +++--- backend/src/game/rulesets/wftda_2025.py | 2 +- backend/src/game/{rosters => skaters}/dependencies.py | 0 backend/src/game/{rosters => skaters}/models.py | 0 backend/src/game/{rosters => skaters}/router.py | 0 backend/src/game/{rosters => skaters}/schemas.py | 0 backend/src/game/teams/models.py | 2 +- 7 files changed, 5 insertions(+), 5 deletions(-) rename backend/src/game/{rosters => skaters}/dependencies.py (100%) rename backend/src/game/{rosters => skaters}/models.py (100%) rename backend/src/game/{rosters => skaters}/router.py (100%) rename backend/src/game/{rosters => skaters}/schemas.py (100%) diff --git a/backend/src/game/__init__.py b/backend/src/game/__init__.py index b96523d2..545b0832 100644 --- a/backend/src/game/__init__.py +++ b/backend/src/game/__init__.py @@ -26,17 +26,17 @@ from .bouts.router import router as bout_router from .jams.router import router as jam_router from .models import CacheableSQLModel, CacheKey -from .rosters.models import Roster -from .rosters.router import router as roster_router from .rulesets import wftda_2025 from .series.models import Series from .series.router import router as series_router +from .skaters.models import Roster +from .skaters.router import router as skater_router from .timeouts.router import router as timeout_router routers: Final[tuple[APIRouter, ...]] = ( bout_router, jam_router, - roster_router, + skater_router, series_router, timeout_router, ) diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index a3cf65de..4cf6e4ca 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -7,7 +7,7 @@ from core.exceptions import GameRulesError, GameStateError from game.bouts.models import REQUIRED_NUM_TEAMS, BaseBout from game.jams.models import BaseJam -from game.rosters.models import Roster +from game.skaters.models import Roster from game.team_jams.models import TeamJam from game.teams.models import BaseTeam from game.timeouts.models import BaseTimeout diff --git a/backend/src/game/rosters/dependencies.py b/backend/src/game/skaters/dependencies.py similarity index 100% rename from backend/src/game/rosters/dependencies.py rename to backend/src/game/skaters/dependencies.py diff --git a/backend/src/game/rosters/models.py b/backend/src/game/skaters/models.py similarity index 100% rename from backend/src/game/rosters/models.py rename to backend/src/game/skaters/models.py diff --git a/backend/src/game/rosters/router.py b/backend/src/game/skaters/router.py similarity index 100% rename from backend/src/game/rosters/router.py rename to backend/src/game/skaters/router.py diff --git a/backend/src/game/rosters/schemas.py b/backend/src/game/skaters/schemas.py similarity index 100% rename from backend/src/game/rosters/schemas.py rename to backend/src/game/skaters/schemas.py diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index a8a7f775..282555ab 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -21,7 +21,7 @@ from sqlalchemy.sql import desc if TYPE_CHECKING: - from game.rosters.models import Roster + from game.skaters.models import Roster REQUIRED_NUM_TEAMS: Final[int] = 2 From 04ab770a7be0cabd3536539e22139a3c3c3e6978 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Fri, 23 Jan 2026 21:56:10 -0800 Subject: [PATCH 032/105] remove dependency --- backend/src/game/skaters/dependencies.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/src/game/skaters/dependencies.py b/backend/src/game/skaters/dependencies.py index fae93872..4262b82d 100644 --- a/backend/src/game/skaters/dependencies.py +++ b/backend/src/game/skaters/dependencies.py @@ -1,10 +1,10 @@ """The FastAPI dependencies methods for Rosters.""" -from typing import Annotated, TypeAlias +from typing import Annotated from core import GetAsyncSession from core.exceptions import ModelLookupError -from fastapi import Depends, Query +from fastapi import Query from sqlalchemy import Result, Select, select from sqlalchemy.exc import NoResultFound @@ -26,6 +26,3 @@ async def _get_roster( # TODO: handle mementos return roster - - -GetRoster: TypeAlias = Annotated[Roster, Depends(_get_roster)] From d771a11f558f8d2c673ea5610f5e1535dd29c226 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Fri, 23 Jan 2026 22:06:26 -0800 Subject: [PATCH 033/105] attach skaters to team --- backend/src/game/skaters/models.py | 34 ++++++++++++------------------ backend/src/game/teams/models.py | 7 ++++++ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/backend/src/game/skaters/models.py b/backend/src/game/skaters/models.py index 47cf0fa0..fcea2b8d 100644 --- a/backend/src/game/skaters/models.py +++ b/backend/src/game/skaters/models.py @@ -2,28 +2,29 @@ from __future__ import annotations -from typing import override +from typing import TYPE_CHECKING, override from uuid import UUID # noqa: TC003 -from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel +from core import CASCADE_OTHER, BaseSQLModel from game.models import CacheableSQLModel, CacheKey -from sqlalchemy import ForeignKey, column +from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship +if TYPE_CHECKING: + from game.teams.models import BaseTeam + class Skater(BaseSQLModel): """Represent a singular Skater in roller derby.""" - roster_uuid: Mapped[UUID] = mapped_column(ForeignKey('rosters.uuid')) + team_uuid: Mapped[UUID] = mapped_column(ForeignKey('teams.uuid')) name: Mapped[str] = mapped_column() pronouns: Mapped[str] = mapped_column() # TODO: Implement pronouns number: Mapped[str] = mapped_column() - _roster: Mapped[Roster] = relationship( - back_populates='skaters', + _team: Mapped[BaseTeam] = relationship( cascade=CASCADE_OTHER, - foreign_keys=[roster_uuid], - lazy='selectin', + foreign_keys=[team_uuid], ) __tablename__: str = 'skaters' @@ -44,16 +45,16 @@ def __init__(self, name: str, number: str) -> None: @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: - return (await self.get_roster(),) + return (await self.get_team(),) - async def get_roster(self) -> Roster: - """Get the Roster to which this Skater belongs. + async def get_team(self) -> BaseTeam: + """Get the Team to which this Skater belongs. Returns: - Roster: the Roster to which this Skater belongs. + BaseTeam: the Team to which this Skater belongs. """ - return await self.awaitable_attrs._roster + return await self.awaitable_attrs._team class Roster(CacheableSQLModel): @@ -63,13 +64,6 @@ class Roster(CacheableSQLModel): league: Mapped[str] = mapped_column() mnemonic: Mapped[str] = mapped_column() - skaters: Mapped[list[Skater]] = relationship( - back_populates='_roster', - cascade=CASCADE_CHILD, - lazy='selectin', - order_by=[column('number')], - ) - __tablename__: str = 'rosters' def __init__(self, name: str, league: str, mnemonic: str = '') -> None: diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index 282555ab..ff38bdb0 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -8,6 +8,7 @@ from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel from game.bouts.models import BaseBout from game.jams.models import BaseJam +from game.skaters.models import Skater from game.team_jams.models import TeamJam from game.timeouts.models import BaseTimeout from sqlalchemy import Constraint, ForeignKey, UniqueConstraint, select @@ -55,6 +56,12 @@ class BaseTeam(BaseSQLModel): cascade=CASCADE_OTHER, foreign_keys=[bout_uuid], ) + skaters: Mapped[list[Skater]] = relationship( + back_populates='_team', + cascade=CASCADE_CHILD, + lazy='selectin', + order_by=[Skater.number], + ) team_jams: Mapped[list[TeamJam]] = relationship( back_populates='_team', cascade=CASCADE_CHILD, From 3cd14c7b4e1dc07d76f97cc3de779df8fd1be66f Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Fri, 23 Jan 2026 22:08:53 -0800 Subject: [PATCH 034/105] remove roster dependency --- backend/src/game/rulesets/wftda_2025.py | 2 +- backend/src/game/teams/models.py | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index 4cf6e4ca..e210b397 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -213,7 +213,7 @@ class Team(_WFTDAModel, BaseTeam): @override def __init__(self, roster: Roster, team_num: int) -> None: - super().__init__(roster, team_num) + super().__init__(team_num) self.timeouts_remaining = Bout.ruleset.num_timeouts self.reviews_remaining = Bout.ruleset.num_reviews diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index ff38bdb0..c9c54c20 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -36,7 +36,6 @@ class BaseTeam(BaseSQLModel): data like a team's score offset. """ - roster_uuid: Mapped[UUID] = mapped_column(ForeignKey('rosters.uuid')) bout_uuid: Mapped[UUID | None] = mapped_column( ForeignKey('bouts.uuid'), nullable=False ) @@ -47,10 +46,6 @@ class BaseTeam(BaseSQLModel): timeouts_remaining: Mapped[int] = mapped_column() reviews_remaining: Mapped[int] = mapped_column() - _roster: Mapped[Roster] = relationship( - cascade=CASCADE_OTHER, - foreign_keys=[roster_uuid], - ) _bout: Mapped[BaseBout | None] = relationship( back_populates='teams', cascade=CASCADE_OTHER, @@ -90,10 +85,7 @@ class BaseTeam(BaseSQLModel): ) __tablename__: str = 'teams' - __table_args__: tuple[Constraint, ...] = ( - UniqueConstraint('bout_uuid', 'roster_uuid'), - UniqueConstraint('bout_uuid', 'num'), - ) + __table_args__: tuple[Constraint, ...] = (UniqueConstraint('bout_uuid', 'num'),) __mapper_args__: dict[str, Any] = { 'polymorphic_abstract': True, 'polymorphic_on': _ruleset, @@ -114,7 +106,7 @@ def get_team_jam_score(cls, team_jam: TeamJam) -> int: """ raise NotImplementedError('BaseTeam.get_team_jam_score() must be overridden') - def __init__(self, roster: Roster, team_num: int) -> None: + def __init__(self, team_num: int) -> None: """Initialize a Team. Args: @@ -124,7 +116,7 @@ def __init__(self, roster: Roster, team_num: int) -> None: a unique team number. A 0 represents the home Team of a Bout. """ - super().__init__(_roster=roster, num=team_num) + super().__init__(num=team_num) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: From 90dc9527338733fd7d37a6be2730db63546c89e8 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Fri, 23 Jan 2026 22:12:20 -0800 Subject: [PATCH 035/105] add constraint --- backend/src/game/skaters/models.py | 5 +++-- backend/src/game/teams/models.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/game/skaters/models.py b/backend/src/game/skaters/models.py index fcea2b8d..0b5c1117 100644 --- a/backend/src/game/skaters/models.py +++ b/backend/src/game/skaters/models.py @@ -7,7 +7,7 @@ from core import CASCADE_OTHER, BaseSQLModel from game.models import CacheableSQLModel, CacheKey -from sqlalchemy import ForeignKey +from sqlalchemy import Constraint, ForeignKey, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship if TYPE_CHECKING: @@ -20,7 +20,7 @@ class Skater(BaseSQLModel): team_uuid: Mapped[UUID] = mapped_column(ForeignKey('teams.uuid')) name: Mapped[str] = mapped_column() pronouns: Mapped[str] = mapped_column() # TODO: Implement pronouns - number: Mapped[str] = mapped_column() + num: Mapped[str] = mapped_column() _team: Mapped[BaseTeam] = relationship( cascade=CASCADE_OTHER, @@ -28,6 +28,7 @@ class Skater(BaseSQLModel): ) __tablename__: str = 'skaters' + __table_args__: tuple[Constraint, ...] = (UniqueConstraint('team_uuid', 'num'),) def __init__(self, name: str, number: str) -> None: """Initialize a Skater. diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index c9c54c20..a8132009 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -55,7 +55,7 @@ class BaseTeam(BaseSQLModel): back_populates='_team', cascade=CASCADE_CHILD, lazy='selectin', - order_by=[Skater.number], + order_by=[Skater.num], ) team_jams: Mapped[list[TeamJam]] = relationship( back_populates='_team', From ab3558b67f1bcf5079e2ae72e9bf169e7086c133 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Fri, 23 Jan 2026 22:19:54 -0800 Subject: [PATCH 036/105] remove unused files --- backend/src/game/skaters/dependencies.py | 28 ------------------------ backend/src/game/skaters/router.py | 8 ++----- backend/src/game/skaters/schemas.py | 14 ------------ backend/src/game/teams/models.py | 3 +++ 4 files changed, 5 insertions(+), 48 deletions(-) delete mode 100644 backend/src/game/skaters/dependencies.py delete mode 100644 backend/src/game/skaters/schemas.py diff --git a/backend/src/game/skaters/dependencies.py b/backend/src/game/skaters/dependencies.py deleted file mode 100644 index 4262b82d..00000000 --- a/backend/src/game/skaters/dependencies.py +++ /dev/null @@ -1,28 +0,0 @@ -"""The FastAPI dependencies methods for Rosters.""" - -from typing import Annotated - -from core import GetAsyncSession -from core.exceptions import ModelLookupError -from fastapi import Query -from sqlalchemy import Result, Select, select -from sqlalchemy.exc import NoResultFound - -from .models import Roster - - -async def _get_roster( - session: GetAsyncSession, - roster_uuid: Annotated[int, Query(alias='rosterUuid')], -) -> Roster: - statement: Select[tuple[Roster]] = select(Roster).where(Roster.uuid == roster_uuid) - results: Result[tuple[Roster]] = await session.execute(statement) - - try: - roster: Roster = results.scalar_one() - except NoResultFound as e: - raise ModelLookupError(f'Could not find Roster ({roster_uuid=})') from e - - # TODO: handle mementos - - return roster diff --git a/backend/src/game/skaters/router.py b/backend/src/game/skaters/router.py index 823c4b4c..6634f21a 100644 --- a/backend/src/game/skaters/router.py +++ b/backend/src/game/skaters/router.py @@ -4,10 +4,6 @@ from fastapi import APIRouter -from .dependencies import _get_roster -from .schemas import RosterSchema +SKATERS_TAG = 'Skater' -ROSTERS_TAG = 'Rosters' - -router: Final[APIRouter] = APIRouter(prefix='/roster') -router.add_api_route('', _get_roster, response_model=RosterSchema, tags=[ROSTERS_TAG]) +router: Final[APIRouter] = APIRouter(prefix='/skater', tags=[SKATERS_TAG]) diff --git a/backend/src/game/skaters/schemas.py b/backend/src/game/skaters/schemas.py deleted file mode 100644 index fb8755f0..00000000 --- a/backend/src/game/skaters/schemas.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Pydantic Roster schemas.""" - -from uuid import UUID - -from core import ServerSchema - - -class RosterSchema(ServerSchema): - """Represent a Roster as a JSON schema.""" - - uuid: UUID - name: str - league: str - mnemonic: str diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index a8132009..e4e2bbf6 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -40,6 +40,9 @@ class BaseTeam(BaseSQLModel): ForeignKey('bouts.uuid'), nullable=False ) + name: Mapped[str] = mapped_column(default='') + league: Mapped[str] = mapped_column(default='') + mnemonic: Mapped[str] = mapped_column(default='') # TODO: Implement Team colors num: Mapped[int] = mapped_column() score_offset: Mapped[int] = mapped_column(default=0) From ef06ca79c0c913c19d6acb63524da0b4a1e05523 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Fri, 23 Jan 2026 22:44:26 -0800 Subject: [PATCH 037/105] remove roster object --- backend/src/game/__init__.py | 2 -- backend/src/game/rulesets/wftda_2025.py | 13 +++---- backend/src/game/skaters/models.py | 47 ++++--------------------- backend/src/game/teams/models.py | 22 +++--------- backend/src/main.py | 7 ++-- 5 files changed, 19 insertions(+), 72 deletions(-) diff --git a/backend/src/game/__init__.py b/backend/src/game/__init__.py index 545b0832..9284a7c3 100644 --- a/backend/src/game/__init__.py +++ b/backend/src/game/__init__.py @@ -29,7 +29,6 @@ from .rulesets import wftda_2025 from .series.models import Series from .series.router import router as series_router -from .skaters.models import Roster from .skaters.router import router as skater_router from .timeouts.router import router as timeout_router @@ -45,7 +44,6 @@ __all__ = ( 'CacheKey', 'CacheableSQLModel', - 'Roster', # Non-rule-bound objects can be exported 'routers', 'Series', # Non-rule-bound objects can be exported 'wftda_2025', diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index e210b397..deafda92 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -1,5 +1,7 @@ """Models and Business logic pertaining to the WFTDA 2025 ruleset.""" +from __future__ import annotations + import logging from datetime import datetime, timedelta from typing import ClassVar, override @@ -7,7 +9,6 @@ from core.exceptions import GameRulesError, GameStateError from game.bouts.models import REQUIRED_NUM_TEAMS, BaseBout from game.jams.models import BaseJam -from game.skaters.models import Roster from game.team_jams.models import TeamJam from game.teams.models import BaseTeam from game.timeouts.models import BaseTimeout @@ -41,11 +42,11 @@ class Bout(_WFTDAModel, BaseBout): ) @override - def __init__(self, home: Roster, away: Roster) -> None: + def __init__(self, home_team_name: str, away_team_name: str) -> None: super().__init__( RULESET_NAME, - Team(home, 0), - Team(away, 1), + Team(home_team_name, 0), + Team(away_team_name, 1), ) self.clock.alarm = timedelta(minutes=30) self.jams.append(Jam(0, 0, *[TeamJam(team) for team in self.teams])) @@ -212,8 +213,8 @@ class Team(_WFTDAModel, BaseTeam): """A Team model using the WFTDA 2025 ruleset.""" @override - def __init__(self, roster: Roster, team_num: int) -> None: - super().__init__(team_num) + def __init__(self, name: str, team_num: int) -> None: + super().__init__(name, team_num) self.timeouts_remaining = Bout.ruleset.num_timeouts self.reviews_remaining = Bout.ruleset.num_reviews diff --git a/backend/src/game/skaters/models.py b/backend/src/game/skaters/models.py index 0b5c1117..09851cb7 100644 --- a/backend/src/game/skaters/models.py +++ b/backend/src/game/skaters/models.py @@ -14,7 +14,7 @@ from game.teams.models import BaseTeam -class Skater(BaseSQLModel): +class Skater(CacheableSQLModel): """Represent a singular Skater in roller derby.""" team_uuid: Mapped[UUID] = mapped_column(ForeignKey('teams.uuid')) @@ -44,6 +44,11 @@ def __init__(self, name: str, number: str) -> None: """ super().__init__(name=name, number=number) + @override + async def cache_key(self) -> CacheKey: + team: BaseTeam = await self.get_team() + return (self.__tablename__, team.bout_uuid, team.num, self.num) + @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: return (await self.get_team(),) @@ -56,43 +61,3 @@ async def get_team(self) -> BaseTeam: """ return await self.awaitable_attrs._team - - -class Roster(CacheableSQLModel): - """Represent a Roster of skaters.""" - - name: Mapped[str] = mapped_column() - league: Mapped[str] = mapped_column() - mnemonic: Mapped[str] = mapped_column() - - __tablename__: str = 'rosters' - - def __init__(self, name: str, league: str, mnemonic: str = '') -> None: - """Initialize a Roster. - - Args: - name (str): the name of this Roster. - league (str): the league to which this Roster belongs. - mnemonic (str, optional): a three to four letter mnemonic of this Roster's - name. When no mnemonic is provided, one will be automatically generated. - Defaults to ''. - - Raises: - ValueError: if a blank Team name is provided. - - """ - name = name.strip() - if name == '': - raise ValueError('Team name cannot be blank') - mnemonic = mnemonic.strip() - if mnemonic == '': - pass # TODO: Implement team name mnemonic algorithm - super().__init__(name=name, league=league, mnemonic=mnemonic) - - @override - async def cache_key(self) -> CacheKey: - return (self.__tablename__, self.uuid) - - @override - async def get_parents(self) -> tuple[BaseSQLModel, ...]: - return () diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index e4e2bbf6..3a12605e 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Final, override +from typing import Any, Final, override from uuid import UUID # noqa: TC003 from core import CASCADE_CHILD, CASCADE_OTHER, BaseSQLModel @@ -21,10 +21,6 @@ ) from sqlalchemy.sql import desc -if TYPE_CHECKING: - from game.skaters.models import Roster - - REQUIRED_NUM_TEAMS: Final[int] = 2 @@ -109,31 +105,21 @@ def get_team_jam_score(cls, team_jam: TeamJam) -> int: """ raise NotImplementedError('BaseTeam.get_team_jam_score() must be overridden') - def __init__(self, team_num: int) -> None: + def __init__(self, name: str, team_num: int) -> None: """Initialize a Team. Args: - bout (BaseBout): the owning Bout of the Team. - roster (Roster): the Roster that this Team will use. + name (str): the name of this Team. team_num (int): the Team number in the Bout. Each Team in a Bout must have a unique team number. A 0 represents the home Team of a Bout. """ - super().__init__(num=team_num) + super().__init__(name=name, num=team_num) @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: return (await self.get_bout(),) - async def get_roster(self) -> Roster: - """Get the Roster to which this Team belongs. - - Returns: - Roster: the Roster to which this Team belongs. - - """ - return await self.awaitable_attrs._roster - async def get_bout(self) -> BaseBout: """Get the Bout to which this Team belongs. diff --git a/backend/src/main.py b/backend/src/main.py index de987400..e988e6c5 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -14,7 +14,7 @@ from core import APIResponseClass, DatabaseEngine, EngineFactory from fastapi import FastAPI from fastapi.concurrency import asynccontextmanager -from game import Roster, Series, wftda_2025 +from game import Series, wftda_2025 from semver import VersionInfo from sqlalchemy import Result, Select, select from update import GithubReleaseSchema @@ -80,10 +80,7 @@ async def lifespan(app: FastAPI): results: Result[tuple[wftda_2025.Bout]] = await session.execute(statement) if results.scalar_one_or_none() is None: logging.info('Instantiating the initial Bout model') - bout: wftda_2025.Bout = wftda_2025.Bout( - Roster('Home', 'Default League'), - Roster('Away', 'Default League'), - ) + bout: wftda_2025.Bout = wftda_2025.Bout('Home', 'Away') series: Series = Series() series.bouts.append(bout) session.add(series) From f68af0c7d34b3fe0636d3418044012cfca718c19 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Fri, 23 Jan 2026 23:02:44 -0800 Subject: [PATCH 038/105] add stub skater schema --- backend/src/game/skaters/schemas.py | 12 ++++++++++++ backend/src/game/teams/schemas.py | 8 +++++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 backend/src/game/skaters/schemas.py diff --git a/backend/src/game/skaters/schemas.py b/backend/src/game/skaters/schemas.py new file mode 100644 index 00000000..102ed4a1 --- /dev/null +++ b/backend/src/game/skaters/schemas.py @@ -0,0 +1,12 @@ +"""Pydantic Skater schemas.""" + +from __future__ import annotations + +from core import ServerSchema + + +class SkaterSchema(ServerSchema): + """Represent a Skater as a JSON schema.""" + + name: str + num: str diff --git a/backend/src/game/teams/schemas.py b/backend/src/game/teams/schemas.py index 234cf1ce..f6e7c386 100644 --- a/backend/src/game/teams/schemas.py +++ b/backend/src/game/teams/schemas.py @@ -2,18 +2,20 @@ from __future__ import annotations -from uuid import UUID # noqa: TC003 - from core import ServerSchema +from game.skaters.schemas import SkaterSchema # noqa: TC002 class TeamSchema(ServerSchema): """Represent a Team as a JSON schema.""" - roster_uuid: UUID + name: str + league: str + mnemonic: str num: int bout_score: int jam_score: int timeouts_remaining: int reviews_remaining: int score_offset: int + skaters: list[SkaterSchema] From dad5fb203e9358271a7f6e3fbcc262d81e5faab6 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 07:54:20 -0800 Subject: [PATCH 039/105] don't eagerly initialize timeouts --- backend/src/game/bouts/models.py | 11 ----------- backend/src/game/bouts/schemas.py | 6 +++--- backend/src/game/rulesets/wftda_2025.py | 10 ++-------- backend/src/game/timeouts/models.py | 18 ++++++++++-------- 4 files changed, 15 insertions(+), 30 deletions(-) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index 683ebe52..dbbfae85 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -169,17 +169,6 @@ def get_running_timeout(self) -> BaseTimeout | None: """ return next((t for t in self.timeouts if t.is_running()), None) - def get_upcoming_timeout(self) -> BaseTimeout | None: - """Get the upcoming Timeout if there is one. - - The upcoming Timeout is the first Timeout that is not started. - - Returns: - BaseTimeout | None: the upcoming Timeout or None. - - """ - return next((t for t in self.timeouts if not t.is_started()), None) - async def begin_period(self, timestamp: datetime) -> None: """Begin the next Period. diff --git a/backend/src/game/bouts/schemas.py b/backend/src/game/bouts/schemas.py index 71212c15..dc6a9a63 100644 --- a/backend/src/game/bouts/schemas.py +++ b/backend/src/game/bouts/schemas.py @@ -31,7 +31,7 @@ class BoutSchema(ServerSchema): @computed_field @property def jam_counts(self) -> tuple[int, int, int]: - """A tuple representing the number of jams in this Bout, per Period. + """A tuple representing the number of jams in this Bout per Period. Returns: tuple[int, int, int]: the number of Jams in each Period of this Bout. @@ -44,8 +44,8 @@ def jam_counts(self) -> tuple[int, int, int]: @computed_field @property - def timeout_counts(self) -> int: - """Get a list representing the IDs of this Bout's Timeouts. + def timeout_count(self) -> int: + """Get the number of Timeouts in this Bout. Returns: list[int]: the number of timeouts in this Bout. diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index deafda92..acca8d2c 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -50,7 +50,6 @@ def __init__(self, home_team_name: str, away_team_name: str) -> None: ) self.clock.alarm = timedelta(minutes=30) self.jams.append(Jam(0, 0, *[TeamJam(team) for team in self.teams])) - self.timeouts.append(Timeout(self, 0)) @override async def begin_period(self, timestamp: datetime) -> None: @@ -169,19 +168,14 @@ async def start_timeout(self, timestamp: datetime) -> BaseTimeout: logging.info(f'Calling Timeout {self}') # Instantiate and start the Timeout - timeout: BaseTimeout | None = self.get_upcoming_timeout() - if timeout is None: - raise NotImplementedError() # FIXME push a new Timeout - + timeout: Timeout = Timeout(len(self.timeouts)) timeout.clock_elapsed = self.clock.get_duration(timestamp) timeout.start(timestamp) + self.timeouts.append(timeout) if self.clock.is_running(): self.clock.stop(timestamp) - # Push a new Timeout to allow users to prefetch it - self.timeouts.append(Timeout(self, timeout.num + 1)) - return timeout @override diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index 1588b004..24b64f42 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -32,7 +32,9 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): _team_uuid: Mapped[UUID | None] = mapped_column(ForeignKey('teams.uuid')) _jam_uuid: Mapped[UUID | None] = mapped_column(ForeignKey('jams.uuid')) - bout_uuid: Mapped[UUID] = mapped_column(ForeignKey('bouts.uuid')) + bout_uuid: Mapped[UUID | None] = mapped_column( + ForeignKey('bouts.uuid'), nullable=False + ) num: Mapped[int] = mapped_column() clock_elapsed: Mapped[timedelta | None] = mapped_column(default=None) @@ -42,21 +44,21 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): result: Mapped[str] = mapped_column(default='') retained: Mapped[bool] = mapped_column(default=False) - _bout: Mapped[BaseBout] = relationship( + _bout: Mapped[BaseBout | None] = relationship( back_populates='timeouts', cascade=CASCADE_OTHER, foreign_keys=[bout_uuid], ) - - # The next two relationships are special cases - they can be eagerly loaded team: Mapped[BaseTeam | None] = relationship( back_populates='timeouts', cascade=CASCADE_OTHER, foreign_keys=[_team_uuid], - lazy='selectin', + lazy='selectin', # Eagerly fetch despite being a parent relationship ) jam: Mapped[BaseJam | None] = relationship( - cascade=CASCADE_OTHER, foreign_keys=[_jam_uuid], lazy='selectin' + cascade=CASCADE_OTHER, + foreign_keys=[_jam_uuid], + lazy='selectin', # Eagerly fetch despite being a parent relationship ) ruleset: MappedSQLExpression[str] = column_property( @@ -81,7 +83,7 @@ def __str__(self) -> str: """ return f'[Bout ID: {self.bout_uuid}, T{self.num}]' - def __init__(self, bout: BaseBout, num: int) -> None: + def __init__(self, num: int) -> None: """Initialize a Timeout. The default state for a Timeout is a regular timeout (not an official review) @@ -92,7 +94,7 @@ def __init__(self, bout: BaseBout, num: int) -> None: num (int): the unique Timeout number associated with this Bout. """ - super().__init__(_bout=bout, num=num) + super().__init__(num=num) @override async def cache_key(self) -> CacheKey: From 455ee3a548b716d487ba16aad0065f46d61f6fd0 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 08:00:13 -0800 Subject: [PATCH 040/105] add method --- backend/src/game/bouts/models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index dbbfae85..cf836367 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -169,6 +169,15 @@ def get_running_timeout(self) -> BaseTimeout | None: """ return next((t for t in self.timeouts if t.is_running()), None) + def get_last_timeout(self) -> BaseTimeout | None: + """Get most recently complete Timeout if there is one. + + Returns: + BaseTimeout | None: the most recently complete Timeout or None. + + """ + return next((t for t in reversed(self.timeouts) if not t.is_running()), None) + async def begin_period(self, timestamp: datetime) -> None: """Begin the next Period. From bc18597dd20551b66f6d022c1565cb594c115e6f Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 08:06:33 -0800 Subject: [PATCH 041/105] remove roster hook --- frontend/hooks/use-roster.ts | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 frontend/hooks/use-roster.ts diff --git a/frontend/hooks/use-roster.ts b/frontend/hooks/use-roster.ts deleted file mode 100644 index 1522a462..00000000 --- a/frontend/hooks/use-roster.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Roster } from "@/lib/game/rosters"; -import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; -import getRoster from "../lib/game/rosters"; - -export const useSuspenseRoster = (rosterId: number) => - useSuspenseQuery({ - queryKey: Roster.generateKey(rosterId), - queryFn: () => getRoster(rosterId), - }); - -export const useRoster = (rosterId: number) => - useQuery({ - queryKey: Roster.generateKey(rosterId), - queryFn: () => getRoster(rosterId), - }); From 4bb4d984408f80f390eaf8822459156f741f1f83 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 19:14:39 -0800 Subject: [PATCH 042/105] update models for backend changes --- frontend/app/main.tsx | 30 ++++---- frontend/app/scoreboard.tsx | 23 +++--- frontend/components/bout-control-buttons.tsx | 19 ++--- frontend/components/bout-state-view.tsx | 2 +- frontend/components/period-clock.tsx | 2 +- frontend/components/team-jam-view.tsx | 2 +- frontend/components/timeout-bar.tsx | 4 +- frontend/components/timeout-buttons.tsx | 4 +- frontend/features/team-view/team-view.tsx | 5 +- frontend/hooks/use-bout.ts | 69 ++++++----------- frontend/hooks/use-jam.ts | 8 +- frontend/hooks/use-ruleset.ts | 8 +- frontend/hooks/use-timeout.ts | 12 +-- frontend/lib/game/bouts.ts | 81 ++++++++++++-------- frontend/lib/game/jams.ts | 14 +++- frontend/lib/game/ruleset.ts | 4 +- frontend/lib/game/timeouts.ts | 21 ++--- frontend/types/ws.ts | 2 +- 18 files changed, 155 insertions(+), 155 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index fe61196a..1c38a18d 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -2,14 +2,13 @@ import BoutControlButtons from "@/components/bout-control-buttons"; import BoutStateView from "@/components/bout-state-view"; import TeamJamView from "@/components/team-jam-view"; import TeamView from "@/features/team-view/team-view"; -import { useSuspenseBout } from "@/hooks/use-bout"; +import { useAllBouts, useBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; -import { useSuspenseSeries } from "@/hooks/use-series"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; -import { useSuspenseTimeout, useTimeout } from "@/hooks/use-timeout"; import queryClient from "@/lib/cache"; -import { Team } from "@/lib/game/bouts"; +import { Bout, Team } from "@/lib/game/bouts"; +import { Timeout } from "@/lib/game/timeouts"; import { redo, undo } from "@/lib/history"; import { BoutContext, JamContext, RulesetContext } from "@/utils/contexts"; import { @@ -23,7 +22,7 @@ import { import "@mantine/core/styles.css"; import { useDisclosure } from "@mantine/hooks"; import { QueryClientProvider } from "@tanstack/react-query"; -import { StrictMode, Suspense } from "react"; +import { StrictMode, Suspense, useEffect } from "react"; import { createRoot } from "react-dom/client"; import "./global.css"; @@ -82,24 +81,27 @@ export default function App() { function Main() { usePrefetchServerTime(); - const { data: series } = useSuspenseSeries(0); - const { data: bout } = useSuspenseBout(series); + const { data: allBouts } = useAllBouts(); - // Eagerly query the latest Jam and Timeout to avoid suspending - void useJam(bout, ...bout.getLatestJamIndex()); - void useTimeout(bout, bout.getLatestTimeoutIndex()); + useEffect(() => { + for (const bout of allBouts) { + queryClient.setQueryData(Bout.generateKey(bout.uuid), bout); + } + }, [allBouts]); - const { data: ruleset } = useSuspenseRuleset(bout); + const { data: bout } = useBout(allBouts[0].uuid); + + // Eagerly query the latest Jam and Timeout to avoid suspending + void useJam(bout, ...bout.getLatestJamNum()); // Fetch Jam data const jamIndex = bout.getActiveOrLatestJamIndex(); const [periodNum, jamNum] = jamIndex; const { data: jam } = useSuspenseJam(bout, periodNum, jamNum); - // Fetch Timeout data - const timeoutIndex = bout.getActiveOrLatestTimeoutIndex(); - const { data: timeout } = useSuspenseTimeout(bout, timeoutIndex); + const timeout = new Timeout(); // FIXME + const { data: ruleset } = useSuspenseRuleset(bout); return ( diff --git a/frontend/app/scoreboard.tsx b/frontend/app/scoreboard.tsx index c8a679e5..9a9e9203 100644 --- a/frontend/app/scoreboard.tsx +++ b/frontend/app/scoreboard.tsx @@ -1,18 +1,17 @@ import BoutStateView from "@/components/bout-state-view"; import TeamView from "@/features/team-view/team-view"; -import { useSuspenseBout } from "@/hooks/use-bout"; +import { useAllBouts, useBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; -import { useSuspenseSeries } from "@/hooks/use-series"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; -import { useSuspenseTimeout, useTimeout } from "@/hooks/use-timeout"; +import { useSuspenseTimeout } from "@/hooks/use-timeout"; import queryClient from "@/lib/cache"; -import { Team } from "@/lib/game/bouts"; +import { Bout, Team } from "@/lib/game/bouts"; import FitScreen from "@fit-screen/react"; import { Center, Grid, MantineProvider, Stack } from "@mantine/core"; import "@mantine/core/styles.css"; import { QueryClientProvider } from "@tanstack/react-query"; -import { StrictMode, Suspense } from "react"; +import { StrictMode, Suspense, useEffect } from "react"; import { createRoot } from "react-dom/client"; import "./global.css"; @@ -38,12 +37,18 @@ export default function App() { function Scoreboard() { usePrefetchServerTime(); - const { data: series } = useSuspenseSeries(0); - const { data: bout } = useSuspenseBout(series); + const { data: allBouts } = useAllBouts(); + + useEffect(() => { + for (const bout of allBouts) { + queryClient.setQueryData(Bout.generateKey(bout.uuid), bout); + } + }, [allBouts]); + + const { data: bout } = useBout(allBouts[0].uuid); // Eagerly query the latest Jam and Timeout to avoid suspending - void useJam(bout, ...bout.getLatestJamIndex()); - void useTimeout(bout, bout.getLatestTimeoutIndex()); + void useJam(bout, ...bout.getLatestJamNum()); const { data: ruleset } = useSuspenseRuleset(bout); diff --git a/frontend/components/bout-control-buttons.tsx b/frontend/components/bout-control-buttons.tsx index adc49d23..dc639518 100644 --- a/frontend/components/bout-control-buttons.tsx +++ b/frontend/components/bout-control-buttons.tsx @@ -1,6 +1,5 @@ import { useBeginPeriod, - useCreateBout, useEndPeriod, useStartJam, useStartTimeout, @@ -9,7 +8,7 @@ import { } from "@/hooks/use-bout"; import { useRedo, useUndo } from "@/hooks/use-history"; import { useSuspenseTimeout } from "@/hooks/use-timeout"; -import { Bout, Team } from "@/lib/game/bouts"; +import { Bout } from "@/lib/game/bouts"; import { ActionIcon, Button, Divider, Grid, Group } from "@mantine/core"; import { IconArrowBackUp, IconArrowForwardUp } from "@tabler/icons-react"; import TimeoutButtons from "./timeout-buttons"; @@ -77,17 +76,17 @@ function StoppedButtons({ bout }: MainControlProps) { let beginPeriodButtonDisabled = false; let beginPeriodText = "Begin Period"; - if (bout.jamIds[2].length > 1) { + if (bout.jamCounts[2] > 1) { beginPeriodButtonDisabled = true; endPeriodButtonText = "End Bout"; - } else if (bout.jamIds[2].length == 1) { + } else if (bout.jamCounts[2] == 1) { beginPeriodText = "Begin OT"; endPeriodButtonText = "End Bout"; startJamText = "Start OT Jam"; - } else if (bout.jamIds[1].length == 1) { + } else if (bout.jamCounts[1] == 1) { endPeriodButtonDisabled = true; beginPeriodText = "Begin P2"; - } else if (bout.jamIds[0].length == 1) { + } else if (bout.jamCounts[0] == 1) { endPeriodButtonDisabled = true; beginPeriodText = "Begin P1"; } @@ -145,7 +144,7 @@ function TimeoutControlButtons({ bout }: MainControlProps) { const startJam = useStartJam(bout); // FIXME: Remove this hook? - const { data } = useSuspenseTimeout(bout, bout.getActiveTimeoutIndex()!); + const { data } = useSuspenseTimeout(bout, bout.getActiveTimeoutNum()!); return ( <> @@ -158,13 +157,9 @@ function TimeoutControlButtons({ bout }: MainControlProps) { } function FinalControlButtons({ bout }: MainControlProps) { - const createBout = useCreateBout(); - - const rosterIds = bout.teams.map((team: Team) => team.rosterId); - return ( <> - + ); } diff --git a/frontend/components/bout-state-view.tsx b/frontend/components/bout-state-view.tsx index 8e9452da..6dfbed7f 100644 --- a/frontend/components/bout-state-view.tsx +++ b/frontend/components/bout-state-view.tsx @@ -33,7 +33,7 @@ export default function BoutStateView({ if (periodNum >= 2) { // Overtime Jams should be considered a continuation of the second half periodNum = 1; - jamNum += bout.jamIds[1].length; + jamNum += bout.jamCounts[1]; } return ( diff --git a/frontend/components/period-clock.tsx b/frontend/components/period-clock.tsx index 78d341e2..c482c989 100644 --- a/frontend/components/period-clock.tsx +++ b/frontend/components/period-clock.tsx @@ -8,7 +8,7 @@ interface PeriodClockProps extends TextProps { } export default function PeriodClock({ bout, ...props }: PeriodClockProps) { - const showClock = bout.jamIds[2].length == 0; + const showClock = bout.jamCounts[2] == 0; const overtimeText = "OT"; diff --git a/frontend/components/team-jam-view.tsx b/frontend/components/team-jam-view.tsx index 9b0ded6c..cd82171c 100644 --- a/frontend/components/team-jam-view.tsx +++ b/frontend/components/team-jam-view.tsx @@ -14,7 +14,7 @@ interface TeamJamViewProps { export default function TeamJamView({ jam, team }: TeamJamViewProps) { const teamJam: TeamJam | undefined = jam.teamJams.find( - (teamJam: TeamJam) => teamJam.teamId === team.id, + (teamJam: TeamJam) => teamJam.teamId === team.num, ); if (teamJam == undefined) { throw new Error("team jam not found"); diff --git a/frontend/components/timeout-bar.tsx b/frontend/components/timeout-bar.tsx index c2a2d040..2b118d1f 100644 --- a/frontend/components/timeout-bar.tsx +++ b/frontend/components/timeout-bar.tsx @@ -56,7 +56,7 @@ export default function TimeoutBar({ invisible={i >= team.timeoutsRemaining} active={ i == team.timeoutsRemaining - 1 && - activeTimeout?.teamId == team.id && + activeTimeout?.teamId == team.num && !activeTimeout?.isReview } /> @@ -74,7 +74,7 @@ export default function TimeoutBar({ invisible={i >= team.reviewsRemaining} active={ i == team.reviewsRemaining - 1 && - activeTimeout?.teamId == team.id && + activeTimeout?.teamId == team.num && activeTimeout?.isReview } /> diff --git a/frontend/components/timeout-buttons.tsx b/frontend/components/timeout-buttons.tsx index d3be1d53..19f8ebe9 100644 --- a/frontend/components/timeout-buttons.tsx +++ b/frontend/components/timeout-buttons.tsx @@ -40,8 +40,8 @@ export default function TimeoutButtons({
- <b>{roster.name}</b> + <b>{team.name}</b>
diff --git a/frontend/hooks/use-bout.ts b/frontend/hooks/use-bout.ts index 2c5fca63..ba52e6fd 100644 --- a/frontend/hooks/use-bout.ts +++ b/frontend/hooks/use-bout.ts @@ -1,49 +1,28 @@ -import { Bout, createBout, getBout } from "@/lib/game/bouts"; -import { Series } from "@/lib/game/series"; -import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; - -export const useSuspenseBout = (series: Series, index?: number) => { - const [boutId, setBoutId] = useState( - index == undefined - ? (series.activeBoutId ?? series.boutIds[series.boutIds.length - 1]) - : series.boutIds[index], - ); - - useEffect(() => { - setBoutId( - index == undefined - ? (series.activeBoutId ?? series.boutIds[series.boutIds.length - 1]) - : series.boutIds[index], - ); - }, [series, index]); - - return useSuspenseQuery({ - queryKey: Bout.generateKey(boutId), - queryFn: () => getBout(boutId), - }); -}; - -export const useBout = (series: Series, index?: number) => { - const [boutId, setBoutId] = useState( - index == undefined - ? (series.activeBoutId ?? series.boutIds[series.boutIds.length - 1]) - : series.boutIds[index], - ); - - useEffect(() => { - setBoutId( - index == undefined - ? (series.activeBoutId ?? series.boutIds[series.boutIds.length - 1]) - : series.boutIds[index], - ); - }, [series, index]); - - return useQuery({ - queryKey: Bout.generateKey(boutId), - queryFn: () => getBout(boutId), +import { Bout, createBout, getAllBouts, getBout } from "@/lib/game/bouts"; +import { + QueryOptions, + useMutation, + useSuspenseQuery, +} from "@tanstack/react-query"; + +export const useAllBouts = () => + useSuspenseQuery({ + queryKey: Bout.generateKey(), + queryFn: () => getAllBouts(), + }); + +export const boutQueryOptions: (uuid: string) => QueryOptions = ( + uuid: string, +) => ({ + queryKey: Bout.generateKey(uuid), + queryFn: () => getBout(uuid), +}); + +export const useBout = (uuid: string) => + useSuspenseQuery({ + queryKey: Bout.generateKey(uuid), + queryFn: () => getBout(uuid), }); -}; // TODO: add ruleset parameter to this hook export const useCreateBout = () => diff --git a/frontend/hooks/use-jam.ts b/frontend/hooks/use-jam.ts index d9fb58c5..4e5d8e37 100644 --- a/frontend/hooks/use-jam.ts +++ b/frontend/hooks/use-jam.ts @@ -4,14 +4,14 @@ import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; export const useSuspenseJam = (bout: Bout, periodNum: number, jamNum: number) => useSuspenseQuery({ - queryKey: Jam.generateKey(bout.id, periodNum, jamNum), - queryFn: () => getJam(bout.jamIds[periodNum][jamNum]), + queryKey: Jam.generateKey(bout.uuid, periodNum, jamNum), + queryFn: () => getJam(bout.uuid, periodNum, jamNum), }); export const useJam = (bout: Bout, periodNum: number, jamNum: number) => useQuery({ - queryKey: Jam.generateKey(bout.id, periodNum, jamNum), - queryFn: () => getJam(bout.jamIds[periodNum][jamNum]), + queryKey: Jam.generateKey(bout.uuid, periodNum, jamNum), + queryFn: () => getJam(bout.uuid, periodNum, jamNum), }); export const useAddTrip = (teamJam: TeamJam) => diff --git a/frontend/hooks/use-ruleset.ts b/frontend/hooks/use-ruleset.ts index 1fb29c0a..688ffe35 100644 --- a/frontend/hooks/use-ruleset.ts +++ b/frontend/hooks/use-ruleset.ts @@ -4,12 +4,12 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; export const useSuspenseRuleset = (bout: Bout) => useSuspenseQuery({ - queryKey: Ruleset.generateKey(bout.ruleset_name), - queryFn: () => getRuleset(bout.id), + queryKey: Ruleset.generateKey(bout.rulesetName), + queryFn: () => getRuleset(bout.uuid), }); export const useRuleset = (bout: Bout) => useQuery({ - queryKey: Ruleset.generateKey(bout.ruleset_name), - queryFn: () => getRuleset(bout.id), + queryKey: Ruleset.generateKey(bout.rulesetName), + queryFn: () => getRuleset(bout.uuid), }); diff --git a/frontend/hooks/use-timeout.ts b/frontend/hooks/use-timeout.ts index 0e931e89..a23d8133 100644 --- a/frontend/hooks/use-timeout.ts +++ b/frontend/hooks/use-timeout.ts @@ -2,16 +2,16 @@ import { Bout } from "@/lib/game/bouts"; import { getTimeout, Timeout } from "@/lib/game/timeouts"; import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; -export const useTimeout = (bout: Bout, index: number) => +export const useTimeout = (bout: Bout, num: number) => useQuery({ - queryKey: Timeout.generateKey(bout.id, index), - queryFn: () => getTimeout(bout.timeoutIds[index]), + queryKey: Timeout.generateKey(bout.uuid, num), + queryFn: () => getTimeout(bout.uuid, num), }); -export const useSuspenseTimeout = (bout: Bout, index: number) => +export const useSuspenseTimeout = (bout: Bout, num: number) => useSuspenseQuery({ - queryKey: Timeout.generateKey(bout.id, index), - queryFn: () => getTimeout(bout.timeoutIds[index]), + queryKey: Timeout.generateKey(bout.uuid, num), + queryFn: () => getTimeout(bout.uuid, num), }); export const useSetType = (timeout: Timeout) => diff --git a/frontend/lib/game/bouts.ts b/frontend/lib/game/bouts.ts index 4c469ca0..390a0157 100644 --- a/frontend/lib/game/bouts.ts +++ b/frontend/lib/game/bouts.ts @@ -2,11 +2,18 @@ import { localAPI } from "@/lib/requests"; import { CacheKey } from "@/types/ws"; import Clock from "./timeouts"; -export async function getBout(boutId: number): Promise { - const data = await localAPI.get>("bout", { query: { boutId } }); +export async function getBout(boutUuid: string): Promise { + const data = await localAPI.get>("bout", { + query: { boutUuid }, + }); return Object.assign(new Bout(), data); } +export async function getAllBouts(): Promise { + const data = await localAPI.get[]>("bout/allBouts"); + return data.map((bout: Partial) => Object.assign(new Bout(), bout)); +} + export async function createBout( rosterIds: number[], seriesIndex = 1, @@ -19,9 +26,9 @@ export async function createBout( } export class Bout { - id: number; - seriesId: number; - ruleset_name: string; + uuid: string; + seriesUuid: string; + rulesetName: string; startCountdown: Date | null; clock: Clock; @@ -30,101 +37,107 @@ export class Bout { isRunning: boolean; isFinal: boolean; teams: Team[]; - jamIds: number[][]; - timeoutIds: number[]; + jamCounts: [number, number, number]; + timeoutCount: number; - static generateKey(boutId: number): CacheKey { - return ["bouts", boutId]; + static generateKey(boutUuid?: string): CacheKey { + if (boutUuid == undefined) { + return ["bouts"]; + } + return ["bouts", boutUuid]; } async beginPeriod(): Promise { - await localAPI.post("bout/beginPeriod", { query: { boutId: this.id } }); + await localAPI.post("bout/beginPeriod", { query: { boutId: this.uuid } }); } async endPeriod(): Promise { - await localAPI.post("bout/endPeriod", { query: { boutId: this.id } }); + await localAPI.post("bout/endPeriod", { query: { boutId: this.uuid } }); } async startJam(): Promise { - await localAPI.post("bout/startJam", { query: { boutId: this.id } }); + await localAPI.post("bout/startJam", { query: { boutId: this.uuid } }); } async stopJam(): Promise { - await localAPI.post("bout/stopJam", { query: { boutId: this.id } }); + await localAPI.post("bout/stopJam", { query: { boutId: this.uuid } }); } async startTimeout(): Promise { - await localAPI.post("bout/startTimeout", { query: { boutId: this.id } }); + await localAPI.post("bout/startTimeout", { query: { boutId: this.uuid } }); } async stopTimeout(): Promise { - await localAPI.post("bout/stopTimeout", { query: { boutId: this.id } }); + await localAPI.post("bout/stopTimeout", { query: { boutId: this.uuid } }); } - getLatestJamIndex(): [number, number] { + getLatestJamNum(): [number, number] { // Get the latest Period number that contains Jams let periodNum = 0; - for (let i = this.jamIds.length - 1; i >= 0; --i) { - if (this.jamIds[i].length > 0) { + for (let i = this.jamCounts.length - 1; i >= 0; --i) { + if (this.jamCounts[i] > 0) { periodNum = i; break; } } // Get the latest Jam number in the active Period - const jamNum = this.jamIds[periodNum].length - 1; + const jamNum = this.jamCounts[periodNum] - 1; return [periodNum, jamNum]; } - getActiveJamIndex(): [number, number] | null { + getActiveJamNum(): [number, number] | null { // Get the latest Period number that contains Jams let periodNum = 0; - for (let i = this.jamIds.length - 1; i >= 0; --i) { - if (this.jamIds[i].length > 0) { + for (let i = this.jamCounts.length - 1; i >= 0; --i) { + if (this.jamCounts[i] > 0) { periodNum = i; break; } } // Get the active Jam number in the active Period - if (this.jamIds[periodNum].length < 2) { + if (this.jamCounts[periodNum] < 2) { return null; // There is no active Jam } - const jamNum = this.jamIds[periodNum].length - 2; + const jamNum = this.jamCounts[periodNum] - 2; return [periodNum, jamNum]; } getActiveOrLatestJamIndex(): [number, number] { - return this.getActiveJamIndex() ?? this.getLatestJamIndex(); + return this.getActiveJamNum() ?? this.getLatestJamNum(); } - getLatestTimeoutIndex(): number { - let timeoutIndex = this.timeoutIds.length - 1; + getLatestTimeoutNum(): number { + let timeoutIndex = this.timeoutCount - 1; if (timeoutIndex < 0) { timeoutIndex = 0; } return timeoutIndex; } - getActiveTimeoutIndex(): number | null { - if (this.timeoutIds.length < 2) { + getActiveTimeoutNum(): number | null { + if (this.timeoutCount < 2) { return null; // There is no active Timeout } - return this.timeoutIds.length - 2; + return this.timeoutCount - 2; } getActiveOrLatestTimeoutIndex(): number { - return this.getActiveTimeoutIndex() ?? this.getLatestTimeoutIndex(); + return this.getActiveTimeoutNum() ?? this.getLatestTimeoutNum(); } } export interface Team { - id: number; - rosterId: number; - boutId: number; + num: number; + + name: string; + league: string; + mnemonic: string; + boutScore: number; jamScore: number; timeoutsRemaining: number; diff --git a/frontend/lib/game/jams.ts b/frontend/lib/game/jams.ts index 6c0ec091..6d7266b6 100644 --- a/frontend/lib/game/jams.ts +++ b/frontend/lib/game/jams.ts @@ -1,8 +1,14 @@ import { localAPI } from "@/lib/requests"; import { CacheKey } from "@/types/ws"; -export async function getJam(jamId: number): Promise { - const data = await localAPI.get>("jam", { query: { jamId } }); +export async function getJam( + boutUuid: string, + period: number, + num: number, +): Promise { + const data = await localAPI.get>("jam", { + query: { boutUuid, period, num }, + }); data.teamJams = data.teamJams?.map((tj) => Object.assign(new TeamJam(), tj)); return Object.assign(new Jam(), data); } @@ -22,11 +28,11 @@ export class Jam { teamJams: TeamJam[]; static generateKey( - boutId: number, + boutUuid: string, periodNum: number, jamNum: number, ): CacheKey { - return ["jams", boutId, periodNum, jamNum]; + return ["jams", boutUuid, periodNum, jamNum]; } hasStarted(): boolean { diff --git a/frontend/lib/game/ruleset.ts b/frontend/lib/game/ruleset.ts index 67f39b0a..b2273c63 100644 --- a/frontend/lib/game/ruleset.ts +++ b/frontend/lib/game/ruleset.ts @@ -1,9 +1,9 @@ import { CacheKey } from "@/types/ws"; import { localAPI } from "../requests"; -export async function getRuleset(boutId: number): Promise { +export async function getRuleset(boutUuid: string): Promise { return localAPI.get("bout/ruleset", { - query: { boutId }, + query: { boutUuid }, }); } diff --git a/frontend/lib/game/timeouts.ts b/frontend/lib/game/timeouts.ts index b534ac0e..7d6c246b 100644 --- a/frontend/lib/game/timeouts.ts +++ b/frontend/lib/game/timeouts.ts @@ -1,9 +1,12 @@ import { CacheKey } from "@/types/ws"; import { localAPI } from "../requests"; -export async function getTimeout(timeoutId: number): Promise { +export async function getTimeout( + boutUuid: string, + num: number, +): Promise { const data = await localAPI.get>("timeout", { - query: { timeoutId }, + query: { boutUuid, num }, }); return Object.assign(new Timeout(), data); } @@ -21,9 +24,9 @@ export default class Clock { } export class Timeout { - id: number; + uuid: string; - boutId: number; + boutUuid: string; num: number; teamId: number | null; jamId: number | null; @@ -37,8 +40,8 @@ export class Timeout { result: string; retained: boolean; - static generateKey(boutId: number, timeoutId: number): CacheKey { - return ["timeouts", boutId, timeoutId]; + static generateKey(boutUuid: string, timeoutId: number): CacheKey { + return ["timeouts", boutUuid, timeoutId]; } hasStarted(): boolean { @@ -51,21 +54,21 @@ export class Timeout { async setType(type: "timeout" | "review"): Promise { await localAPI.post("timeout/type", { - query: { timeoutId: this.id }, + query: { timeoutId: this.uuid }, body: JSON.stringify(type), }); } async setTeam(team: number | null): Promise { await localAPI.post("timeout/team", { - query: { timeoutId: this.id }, + query: { timeoutId: this.uuid }, body: team, }); } async setRetained(isRetained: boolean): Promise { await localAPI.post("timeout/retained", { - query: { timeoutId: this.id }, + query: { timeoutId: this.uuid }, body: isRetained, }); } diff --git a/frontend/types/ws.ts b/frontend/types/ws.ts index 238053ff..570eca77 100644 --- a/frontend/types/ws.ts +++ b/frontend/types/ws.ts @@ -1,4 +1,4 @@ -export type CacheKey = [string, ...number[]] | [string, string]; +export type CacheKey = [string, ...unknown[]] | [string, string]; export interface ServerData { process: Date | null; From 0f6b95c1eff83f417230c97af63a4e251194f446 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 20:09:39 -0800 Subject: [PATCH 043/105] remove unused file --- frontend/lib/game/rosters.ts | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 frontend/lib/game/rosters.ts diff --git a/frontend/lib/game/rosters.ts b/frontend/lib/game/rosters.ts deleted file mode 100644 index bb2f95ff..00000000 --- a/frontend/lib/game/rosters.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { localAPI } from "@/lib/requests"; -import { CacheKey } from "@/types/ws"; - -export default async function getRoster(rosterId: number): Promise { - const data = await localAPI.get>("roster", { - query: { rosterId }, - }); - return Object.assign(new Roster(), data); -} - -export class Roster { - id: number; - name: string; - - static generateKey(rosterId: number): CacheKey { - return ["rosters", rosterId]; - } -} From 86173c54a06f5f6ff89f811b8d7dadd6dcedb003 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 20:21:37 -0800 Subject: [PATCH 044/105] remove old id --- backend/src/game/trip_events/schemas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/game/trip_events/schemas.py b/backend/src/game/trip_events/schemas.py index fbeb5be4..ebc3f8c3 100644 --- a/backend/src/game/trip_events/schemas.py +++ b/backend/src/game/trip_events/schemas.py @@ -8,7 +8,6 @@ class TripEventSchema(ServerSchema): """Represent a TripEvent as a JSON schema.""" - id: int timestamp: datetime lead: bool lost: bool From 5b86da3480fd213b2b51d5f08c295959bbbc6aac Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 20:49:23 -0800 Subject: [PATCH 045/105] update models to match backend --- frontend/components/team-jam-view.tsx | 2 +- frontend/lib/game/jams.ts | 48 +++++++++++++++++++++------ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/frontend/components/team-jam-view.tsx b/frontend/components/team-jam-view.tsx index cd82171c..6e8f3745 100644 --- a/frontend/components/team-jam-view.tsx +++ b/frontend/components/team-jam-view.tsx @@ -14,7 +14,7 @@ interface TeamJamViewProps { export default function TeamJamView({ jam, team }: TeamJamViewProps) { const teamJam: TeamJam | undefined = jam.teamJams.find( - (teamJam: TeamJam) => teamJam.teamId === team.num, + (teamJam: TeamJam) => teamJam.teamNum === team.num, ); if (teamJam == undefined) { throw new Error("team jam not found"); diff --git a/frontend/lib/game/jams.ts b/frontend/lib/game/jams.ts index 6d7266b6..d3dfe817 100644 --- a/frontend/lib/game/jams.ts +++ b/frontend/lib/game/jams.ts @@ -16,8 +16,7 @@ export async function getJam( export type StopReasonString = "called" | "elapsed" | "injury" | "other"; export class Jam { - id: number; - boutId: number; + boutUuid: string; period: number; num: number; @@ -28,9 +27,9 @@ export class Jam { teamJams: TeamJam[]; static generateKey( - boutUuid: string, - periodNum: number, - jamNum: number, + boutUuid?: string, + periodNum?: number, + jamNum?: number, ): CacheKey { return ["jams", boutUuid, periodNum, jamNum]; } @@ -42,37 +41,64 @@ export class Jam { isRunning(): boolean { return this.hasStarted() && this.stopTimestamp == null; } + + async addTrip(passes: number) { + await localAPI.post("jam/addTrip", { + // query: { jamId: this.jamId, teamId: this.teamId }, + body: passes, + }); + } + + async setLead(lead: boolean) { + await localAPI.post("jam/setLead", { + // query: { jamId: this.jamId, teamId: this.teamId }, + body: lead, + }); + } + + async setLost(lost: boolean) { + await localAPI.post("jam/setLost", { + // query: { jamId: this.jamId, teamId: this.teamId }, + body: lost, + }); + } + + async setStarPass(starPass: boolean) { + await localAPI.post("jam/setStarPass", { + // query: { jamId: this.jamId, teamId: this.teamId }, + body: starPass, + }); + } } export class TeamJam { - jamId: number; - teamId: number; + teamNum: number; events: TripEvent[]; async addTrip(passes: number) { await localAPI.post("jam/addTrip", { - query: { jamId: this.jamId, teamId: this.teamId }, + // query: { jamId: this.jamId, teamId: this.teamId }, // FIXME body: passes, }); } async setLead(lead: boolean) { await localAPI.post("jam/setLead", { - query: { jamId: this.jamId, teamId: this.teamId }, + // query: { jamId: this.jamId, teamId: this.teamId }, // FIXME body: lead, }); } async setLost(lost: boolean) { await localAPI.post("jam/setLost", { - query: { jamId: this.jamId, teamId: this.teamId }, + // query: { jamId: this.jamId, teamId: this.teamId }, // FIXME body: lost, }); } async setStarPass(starPass: boolean) { await localAPI.post("jam/setStarPass", { - query: { jamId: this.jamId, teamId: this.teamId }, + // query: { jamId: this.jamId, teamId: this.teamId }, // FIXME body: starPass, }); } From e75a13f4c6c214a0ac9f6b59d8653d5eb92c38c1 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 20:54:20 -0800 Subject: [PATCH 046/105] update Timeout model to match backend --- frontend/components/extraordinary-state-clock.tsx | 2 +- frontend/components/timeout-bar.tsx | 4 ++-- frontend/components/timeout-buttons.tsx | 4 ++-- frontend/lib/game/timeouts.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/components/extraordinary-state-clock.tsx b/frontend/components/extraordinary-state-clock.tsx index 174d521d..17f97cd5 100644 --- a/frontend/components/extraordinary-state-clock.tsx +++ b/frontend/components/extraordinary-state-clock.tsx @@ -37,7 +37,7 @@ export default function ExtraordinaryStateClock({ gameState = "Official Timeout"; } else if (latestTimeout?.isReview) { gameState = "Official Review"; - } else if (latestTimeout?.teamId != null) { + } else if (latestTimeout?.teamNum != null) { gameState = "Team Timeout"; } else { gameState = "Timeout"; diff --git a/frontend/components/timeout-bar.tsx b/frontend/components/timeout-bar.tsx index 2b118d1f..21fb2b05 100644 --- a/frontend/components/timeout-bar.tsx +++ b/frontend/components/timeout-bar.tsx @@ -56,7 +56,7 @@ export default function TimeoutBar({ invisible={i >= team.timeoutsRemaining} active={ i == team.timeoutsRemaining - 1 && - activeTimeout?.teamId == team.num && + activeTimeout?.teamNum == team.num && !activeTimeout?.isReview } /> @@ -74,7 +74,7 @@ export default function TimeoutBar({ invisible={i >= team.reviewsRemaining} active={ i == team.reviewsRemaining - 1 && - activeTimeout?.teamId == team.num && + activeTimeout?.teamNum == team.num && activeTimeout?.isReview } /> diff --git a/frontend/components/timeout-buttons.tsx b/frontend/components/timeout-buttons.tsx index 19f8ebe9..468639b2 100644 --- a/frontend/components/timeout-buttons.tsx +++ b/frontend/components/timeout-buttons.tsx @@ -49,11 +49,11 @@ export default function TimeoutButtons({ }, ]} value={ - timeout.teamId == null + timeout.teamNum == null ? timeout.teamIsOfficials ? String(NaN) : "" - : String(timeout.teamId) + : String(timeout.teamNum) } onChange={(teamId) => setTeam.mutate(teamId == String(NaN) ? null : Number(teamId)) diff --git a/frontend/lib/game/timeouts.ts b/frontend/lib/game/timeouts.ts index 7d6c246b..3f01fe32 100644 --- a/frontend/lib/game/timeouts.ts +++ b/frontend/lib/game/timeouts.ts @@ -28,12 +28,12 @@ export class Timeout { boutUuid: string; num: number; - teamId: number | null; - jamId: number | null; + startTimestamp: Date | null; stopTimestamp: Date | null; clockElapsed: number; + teamNum: number | null; teamIsOfficials: boolean; isReview: boolean; details: string; From 0a1381463f0f7d96d5699befb8a8c91aeb36b65a Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 21:28:19 -0800 Subject: [PATCH 047/105] all timeouts require a jam reference --- backend/src/game/bouts/models.py | 9 +++++++++ backend/src/game/rulesets/wftda_2025.py | 2 +- backend/src/game/timeouts/models.py | 22 +++++++++++----------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/backend/src/game/bouts/models.py b/backend/src/game/bouts/models.py index cf836367..79947655 100644 --- a/backend/src/game/bouts/models.py +++ b/backend/src/game/bouts/models.py @@ -140,6 +140,15 @@ def state(self) -> Literal['final', 'jam', 'lineup', 'stopped', 'timeout']: else: return 'stopped' + def get_active_jam(self) -> BaseJam: + """Get the most recently started Jam or upcoming Jam. + + Returns: + BaseJam: the active Jam. + + """ + return next((j for j in self.jams if j.is_started()), self.jams[-1]) + def get_running_jam(self) -> BaseJam | None: """Get the running Jam if there is one. diff --git a/backend/src/game/rulesets/wftda_2025.py b/backend/src/game/rulesets/wftda_2025.py index acca8d2c..b2f4266b 100644 --- a/backend/src/game/rulesets/wftda_2025.py +++ b/backend/src/game/rulesets/wftda_2025.py @@ -168,7 +168,7 @@ async def start_timeout(self, timestamp: datetime) -> BaseTimeout: logging.info(f'Calling Timeout {self}') # Instantiate and start the Timeout - timeout: Timeout = Timeout(len(self.timeouts)) + timeout: Timeout = Timeout(self.get_active_jam(), len(self.timeouts)) timeout.clock_elapsed = self.clock.get_duration(timestamp) timeout.start(timestamp) self.timeouts.append(timeout) diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index 24b64f42..03570fd5 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -30,8 +30,8 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): Timeouts models can represent either a timeout or an official review. """ + _jam_uuid: Mapped[UUID] = mapped_column(ForeignKey('jams.uuid')) _team_uuid: Mapped[UUID | None] = mapped_column(ForeignKey('teams.uuid')) - _jam_uuid: Mapped[UUID | None] = mapped_column(ForeignKey('jams.uuid')) bout_uuid: Mapped[UUID | None] = mapped_column( ForeignKey('bouts.uuid'), nullable=False ) @@ -49,19 +49,19 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): cascade=CASCADE_OTHER, foreign_keys=[bout_uuid], ) - team: Mapped[BaseTeam | None] = relationship( - back_populates='timeouts', + jam: Mapped[BaseJam] = relationship( cascade=CASCADE_OTHER, - foreign_keys=[_team_uuid], + foreign_keys=[_jam_uuid], lazy='selectin', # Eagerly fetch despite being a parent relationship ) - jam: Mapped[BaseJam | None] = relationship( + team: Mapped[BaseTeam | None] = relationship( + back_populates='timeouts', cascade=CASCADE_OTHER, - foreign_keys=[_jam_uuid], + foreign_keys=[_team_uuid], lazy='selectin', # Eagerly fetch despite being a parent relationship ) - ruleset: MappedSQLExpression[str] = column_property( + _ruleset: MappedSQLExpression[str] = column_property( select(BaseBout.ruleset_name) .where(BaseBout.uuid == bout_uuid) .scalar_subquery() @@ -70,7 +70,7 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): __tablename__: str = 'timeouts' __mapper_args__: dict[str, Any] = { 'polymorphic_abstract': True, - 'polymorphic_on': ruleset, + 'polymorphic_on': _ruleset, } __table_args__: tuple[Constraint, ...] = (UniqueConstraint('bout_uuid', 'num'),) @@ -83,18 +83,18 @@ def __str__(self) -> str: """ return f'[Bout ID: {self.bout_uuid}, T{self.num}]' - def __init__(self, num: int) -> None: + def __init__(self, jam: BaseJam, num: int) -> None: """Initialize a Timeout. The default state for a Timeout is a regular timeout (not an official review) with an unknown caller (called by neither a Team nor by the officials). Args: - bout (BaseBout): the Bout that owns this Timeout. + jam (BaseJam): the Jam preceding this Timeout num (int): the unique Timeout number associated with this Bout. """ - super().__init__(num=num) + super().__init__(_jam=jam, num=num) @override async def cache_key(self) -> CacheKey: From a158fe834535e6553ec77b65f74802d2499dbf8c Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 21:43:37 -0800 Subject: [PATCH 048/105] formatting fixes --- backend/src/game/jams/models.py | 1 + backend/src/game/skaters/models.py | 3 ++- backend/src/game/teams/models.py | 5 +++-- backend/src/game/timeouts/models.py | 1 + backend/src/game/trip_events/models.py | 6 ++++-- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/src/game/jams/models.py b/backend/src/game/jams/models.py index 802e2c81..3df73b08 100644 --- a/backend/src/game/jams/models.py +++ b/backend/src/game/jams/models.py @@ -36,6 +36,7 @@ class BaseJam(AbstractOneShotModel, CacheableSQLModel): num: Mapped[int] = mapped_column(index=True) period: Mapped[int] = mapped_column(index=True) + stop_reason: Mapped[StopReasonStr | None] = mapped_column(default=None) _bout: Mapped[BaseBout | None] = relationship( diff --git a/backend/src/game/skaters/models.py b/backend/src/game/skaters/models.py index 09851cb7..014aef70 100644 --- a/backend/src/game/skaters/models.py +++ b/backend/src/game/skaters/models.py @@ -18,9 +18,10 @@ class Skater(CacheableSQLModel): """Represent a singular Skater in roller derby.""" team_uuid: Mapped[UUID] = mapped_column(ForeignKey('teams.uuid')) + + num: Mapped[str] = mapped_column() name: Mapped[str] = mapped_column() pronouns: Mapped[str] = mapped_column() # TODO: Implement pronouns - num: Mapped[str] = mapped_column() _team: Mapped[BaseTeam] = relationship( cascade=CASCADE_OTHER, diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index 3a12605e..94e87e44 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -36,11 +36,12 @@ class BaseTeam(BaseSQLModel): ForeignKey('bouts.uuid'), nullable=False ) - name: Mapped[str] = mapped_column(default='') + num: Mapped[int] = mapped_column() + + name: Mapped[str] = mapped_column() league: Mapped[str] = mapped_column(default='') mnemonic: Mapped[str] = mapped_column(default='') # TODO: Implement Team colors - num: Mapped[int] = mapped_column() score_offset: Mapped[int] = mapped_column(default=0) timeouts_remaining: Mapped[int] = mapped_column() reviews_remaining: Mapped[int] = mapped_column() diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index 03570fd5..5875addb 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -37,6 +37,7 @@ class BaseTimeout(AbstractOneShotModel, CacheableSQLModel): ) num: Mapped[int] = mapped_column() + clock_elapsed: Mapped[timedelta | None] = mapped_column(default=None) team_is_officials: Mapped[bool] = mapped_column(default=False) is_review: Mapped[bool] = mapped_column(default=False) diff --git a/backend/src/game/trip_events/models.py b/backend/src/game/trip_events/models.py index 3c26d104..c15f3d1d 100644 --- a/backend/src/game/trip_events/models.py +++ b/backend/src/game/trip_events/models.py @@ -27,7 +27,7 @@ class TripEvent(BaseSQLModel): eligibility. """ - _team_jam_uuid: Mapped[UUID | None] = mapped_column( + team_jam_uuid: Mapped[UUID | None] = mapped_column( ForeignKey('team_jams.uuid'), nullable=False ) @@ -38,7 +38,9 @@ class TripEvent(BaseSQLModel): star_pass: Mapped[bool] = mapped_column(default=False) _team_jam: Mapped[TeamJam | None] = relationship( - back_populates='events', cascade=CASCADE_OTHER, foreign_keys=[_team_jam_uuid] + back_populates='events', + cascade=CASCADE_OTHER, + foreign_keys=[team_jam_uuid], ) __tablename__: str = 'trip_events' From 23dcff6db6be838900244807ba5def9cfec3f371 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 21:50:36 -0800 Subject: [PATCH 049/105] add period num and jam num to timeout --- backend/src/game/timeouts/schemas.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/backend/src/game/timeouts/schemas.py b/backend/src/game/timeouts/schemas.py index e67ac007..9377f6ec 100644 --- a/backend/src/game/timeouts/schemas.py +++ b/backend/src/game/timeouts/schemas.py @@ -5,6 +5,7 @@ from uuid import UUID from core import ServerSchema, timedelta_serializer +from game.jams.schemas import JamSchema from game.teams.schemas import TeamSchema from pydantic import Field, computed_field @@ -15,8 +16,8 @@ class TimeoutSchema(ServerSchema): bout_uuid: UUID num: int + jam: JamSchema = Field(exclude=True) team: TeamSchema | None = Field(exclude=True) - # FIXME: add period num and jam num, if any start_timestamp: datetime | None stop_timestamp: datetime | None @@ -28,6 +29,28 @@ class TimeoutSchema(ServerSchema): result: str retained: bool + @computed_field + @property + def period_num(self) -> int: + """Get the Period number of the Jam preceding this Timeout. + + Returns: + int: the Period number of the Jam preceding this Timeout. + + """ + return self.jam.period + + @computed_field + @property + def jam_num(self) -> int | None: + """Get the Jam number of the Jam preceding this Timeout. + + Returns: + int: the Jam number of the Jam preceding this Timeout. + + """ + return self.jam.num + @computed_field @property def team_num(self) -> int | None: From b639c997d0398bfb9e5e07d61e64ba9df803c95d Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 26 Jan 2026 21:57:37 -0800 Subject: [PATCH 050/105] docs --- backend/src/game/timeouts/schemas.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/game/timeouts/schemas.py b/backend/src/game/timeouts/schemas.py index 9377f6ec..f59807bf 100644 --- a/backend/src/game/timeouts/schemas.py +++ b/backend/src/game/timeouts/schemas.py @@ -34,6 +34,9 @@ class TimeoutSchema(ServerSchema): def period_num(self) -> int: """Get the Period number of the Jam preceding this Timeout. + Timeouts cannot be uniquely identified by their Period and Jam number because + multiple Timeouts may be called after a single Jam. + Returns: int: the Period number of the Jam preceding this Timeout. @@ -45,6 +48,9 @@ def period_num(self) -> int: def jam_num(self) -> int | None: """Get the Jam number of the Jam preceding this Timeout. + Timeouts cannot be uniquely identified by their Period and Jam number because + multiple Timeouts may be called after a single Jam. + Returns: int: the Jam number of the Jam preceding this Timeout. From 223e45d0414ad971a72bdfb79e7801cd98e5da7f Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 27 Jan 2026 13:04:17 -0800 Subject: [PATCH 051/105] upgrade deps --- uv.lock | 58 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/uv.lock b/uv.lock index 6c2fe18f..11f7c092 100644 --- a/uv.lock +++ b/uv.lock @@ -706,11 +706,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.21" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -757,15 +757,15 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, ] [[package]] @@ -855,11 +855,11 @@ wheels = [ [[package]] name = "setuptools" -version = "80.10.1" +version = "80.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/ff/f75651350db3cf2ef767371307eb163f3cc1ac03e16fdf3ac347607f7edb/setuptools-80.10.1.tar.gz", hash = "sha256:bf2e513eb8144c3298a3bd28ab1a5edb739131ec5c22e045ff93cd7f5319703a", size = 1229650, upload-time = "2026-01-21T09:42:03.061Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/76/f963c61683a39084aa575f98089253e1e852a4417cb8a3a8a422923a5246/setuptools-80.10.1-py3-none-any.whl", hash = "sha256:fc30c51cbcb8199a219c12cc9c281b5925a4978d212f84229c909636d9f6984e", size = 1099859, upload-time = "2026-01-21T09:42:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, ] [[package]] @@ -926,26 +926,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/dc/b607f00916f5a7c52860b84a66dc17bc6988e8445e96b1d6e175a3837397/ty-0.0.13.tar.gz", hash = "sha256:7a1d135a400ca076407ea30012d1f75419634160ed3b9cad96607bf2956b23b3", size = 4999183, upload-time = "2026-01-21T13:21:16.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/df/3632f1918f4c0a33184f107efc5d436ab6da147fd3d3b94b3af6461efbf4/ty-0.0.13-py3-none-linux_armv6l.whl", hash = "sha256:1b2b8e02697c3a94c722957d712a0615bcc317c9b9497be116ef746615d892f2", size = 9993501, upload-time = "2026-01-21T13:21:26.628Z" }, - { url = "https://files.pythonhosted.org/packages/92/87/6a473ced5ac280c6ce5b1627c71a8a695c64481b99aabc798718376a441e/ty-0.0.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f15cdb8e233e2b5adfce673bb21f4c5e8eaf3334842f7eea3c70ac6fda8c1de5", size = 9860986, upload-time = "2026-01-21T13:21:24.425Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9b/d89ae375cf0a7cd9360e1164ce017f8c753759be63b6a11ed4c944abe8c6/ty-0.0.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0819e89ac9f0d8af7a062837ce197f0461fee2fc14fd07e2c368780d3a397b73", size = 9350748, upload-time = "2026-01-21T13:21:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a6/9ad58518056fab344b20c0bb2c1911936ebe195318e8acc3bc45ac1c6b6b/ty-0.0.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de79f481084b7cc7a202ba0d7a75e10970d10ffa4f025b23f2e6b7324b74886", size = 9849884, upload-time = "2026-01-21T13:21:21.886Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c3/8add69095fa179f523d9e9afcc15a00818af0a37f2b237a9b59bc0046c34/ty-0.0.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4fb2154cff7c6e95d46bfaba283c60642616f20d73e5f96d0c89c269f3e1bcec", size = 9822975, upload-time = "2026-01-21T13:21:14.292Z" }, - { url = "https://files.pythonhosted.org/packages/a4/05/4c0927c68a0a6d43fb02f3f0b6c19c64e3461dc8ed6c404dde0efb8058f7/ty-0.0.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00be58d89337c27968a20d58ca553458608c5b634170e2bec82824c2e4cf4d96", size = 10294045, upload-time = "2026-01-21T13:21:30.505Z" }, - { url = "https://files.pythonhosted.org/packages/b4/86/6dc190838aba967557fe0bfd494c595d00b5081315a98aaf60c0e632aaeb/ty-0.0.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72435eade1fa58c6218abb4340f43a6c3ff856ae2dc5722a247d3a6dd32e9737", size = 10916460, upload-time = "2026-01-21T13:21:07.788Z" }, - { url = "https://files.pythonhosted.org/packages/04/40/9ead96b7c122e1109dfcd11671184c3506996bf6a649306ec427e81d9544/ty-0.0.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77a548742ee8f621d718159e7027c3b555051d096a49bb580249a6c5fc86c271", size = 10597154, upload-time = "2026-01-21T13:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7d/e832a2c081d2be845dc6972d0c7998914d168ccbc0b9c86794419ab7376e/ty-0.0.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da067c57c289b7cf914669704b552b6207c2cc7f50da4118c3e12388642e6b3f", size = 10410710, upload-time = "2026-01-21T13:21:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/31/e3/898be3a96237a32f05c4c29b43594dc3b46e0eedfe8243058e46153b324f/ty-0.0.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d1b50a01fffa140417fca5a24b658fbe0734074a095d5b6f0552484724474343", size = 9826299, upload-time = "2026-01-21T13:21:00.845Z" }, - { url = "https://files.pythonhosted.org/packages/bb/eb/db2d852ce0ed742505ff18ee10d7d252f3acfd6fc60eca7e9c7a0288a6d8/ty-0.0.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f33c46f52e5e9378378eca0d8059f026f3c8073ace02f7f2e8d079ddfe5207e", size = 9831610, upload-time = "2026-01-21T13:21:05.842Z" }, - { url = "https://files.pythonhosted.org/packages/9e/61/149f59c8abaddcbcbb0bd13b89c7741ae1c637823c5cf92ed2c644fcadef/ty-0.0.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:168eda24d9a0b202cf3758c2962cc295878842042b7eca9ed2965259f59ce9f2", size = 9978885, upload-time = "2026-01-21T13:21:10.306Z" }, - { url = "https://files.pythonhosted.org/packages/a0/cd/026d4e4af60a80918a8d73d2c42b8262dd43ab2fa7b28d9743004cb88d57/ty-0.0.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4917678b95dc8cb399cc459fab568ba8d5f0f33b7a94bf840d9733043c43f29", size = 10506453, upload-time = "2026-01-21T13:20:56.633Z" }, - { url = "https://files.pythonhosted.org/packages/63/06/8932833a4eca2df49c997a29afb26721612de8078ae79074c8fe87e17516/ty-0.0.13-py3-none-win32.whl", hash = "sha256:c1f2ec40daa405508b053e5b8e440fbae5fdb85c69c9ab0ee078f8bc00eeec3d", size = 9433482, upload-time = "2026-01-21T13:20:58.717Z" }, - { url = "https://files.pythonhosted.org/packages/aa/fd/e8d972d1a69df25c2cecb20ea50e49ad5f27a06f55f1f5f399a563e71645/ty-0.0.13-py3-none-win_amd64.whl", hash = "sha256:8b7b1ab9f187affbceff89d51076038363b14113be29bda2ddfa17116de1d476", size = 10319156, upload-time = "2026-01-21T13:21:03.266Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c2/05fdd64ac003a560d4fbd1faa7d9a31d75df8f901675e5bed1ee2ceeff87/ty-0.0.13-py3-none-win_arm64.whl", hash = "sha256:1c9630333497c77bb9bcabba42971b96ee1f36c601dd3dcac66b4134f9fa38f0", size = 9808316, upload-time = "2026-01-21T13:20:54.053Z" }, +version = "0.0.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/57/22c3d6bf95c2229120c49ffc2f0da8d9e8823755a1c3194da56e51f1cc31/ty-0.0.14.tar.gz", hash = "sha256:a691010565f59dd7f15cf324cdcd1d9065e010c77a04f887e1ea070ba34a7de2", size = 5036573, upload-time = "2026-01-27T00:57:31.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/cb/cc6d1d8de59beb17a41f9a614585f884ec2d95450306c173b3b7cc090d2e/ty-0.0.14-py3-none-linux_armv6l.whl", hash = "sha256:32cf2a7596e693094621d3ae568d7ee16707dce28c34d1762947874060fdddaa", size = 10034228, upload-time = "2026-01-27T00:57:53.133Z" }, + { url = "https://files.pythonhosted.org/packages/f3/96/dd42816a2075a8f31542296ae687483a8d047f86a6538dfba573223eaf9a/ty-0.0.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f971bf9805f49ce8c0968ad53e29624d80b970b9eb597b7cbaba25d8a18ce9a2", size = 9939162, upload-time = "2026-01-27T00:57:43.857Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b4/73c4859004e0f0a9eead9ecb67021438b2e8e5fdd8d03e7f5aca77623992/ty-0.0.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:45448b9e4806423523268bc15e9208c4f3f2ead7c344f615549d2e2354d6e924", size = 9418661, upload-time = "2026-01-27T00:58:03.411Z" }, + { url = "https://files.pythonhosted.org/packages/58/35/839c4551b94613db4afa20ee555dd4f33bfa7352d5da74c5fa416ffa0fd2/ty-0.0.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94a9b747ff40114085206bdb3205a631ef19a4d3fb89e302a88754cbbae54c", size = 9837872, upload-time = "2026-01-27T00:57:23.718Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/bbecf7e2faa20c04bebd35fc478668953ca50ee5847ce23e08acf20ea119/ty-0.0.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6756715a3c33182e9ab8ffca2bb314d3c99b9c410b171736e145773ee0ae41c3", size = 9848819, upload-time = "2026-01-27T00:57:58.501Z" }, + { url = "https://files.pythonhosted.org/packages/be/60/3c0ba0f19c0f647ad9d2b5b5ac68c0f0b4dc899001bd53b3a7537fb247a2/ty-0.0.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89d0038a2f698ba8b6fec5cf216a4e44e2f95e4a5095a8c0f57fe549f87087c2", size = 10324371, upload-time = "2026-01-27T00:57:29.291Z" }, + { url = "https://files.pythonhosted.org/packages/24/32/99d0a0b37d0397b0a989ffc2682493286aa3bc252b24004a6714368c2c3d/ty-0.0.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c64a83a2d669b77f50a4957039ca1450626fb474619f18f6f8a3eb885bf7544", size = 10865898, upload-time = "2026-01-27T00:57:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/1a/88/30b583a9e0311bb474269cfa91db53350557ebec09002bfc3fb3fc364e8c/ty-0.0.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:242488bfb547ef080199f6fd81369ab9cb638a778bb161511d091ffd49c12129", size = 10555777, upload-time = "2026-01-27T00:58:05.853Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a2/cb53fb6325dcf3d40f2b1d0457a25d55bfbae633c8e337bde8ec01a190eb/ty-0.0.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4790c3866f6c83a4f424fc7d09ebdb225c1f1131647ba8bdc6fcdc28f09ed0ff", size = 10412913, upload-time = "2026-01-27T00:57:38.834Z" }, + { url = "https://files.pythonhosted.org/packages/42/8f/f2f5202d725ed1e6a4e5ffaa32b190a1fe70c0b1a2503d38515da4130b4c/ty-0.0.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:950f320437f96d4ea9a2332bbfb5b68f1c1acd269ebfa4c09b6970cc1565bd9d", size = 9837608, upload-time = "2026-01-27T00:57:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/59a2a0521640c489dafa2c546ae1f8465f92956fede18660653cce73b4c5/ty-0.0.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a0ec3ee70d83887f86925bbc1c56f4628bd58a0f47f6f32ddfe04e1f05466df", size = 9884324, upload-time = "2026-01-27T00:57:46.786Z" }, + { url = "https://files.pythonhosted.org/packages/03/95/8d2a49880f47b638743212f011088552ecc454dd7a665ddcbdabea25772a/ty-0.0.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1a4e6b6da0c58b34415955279eff754d6206b35af56a18bb70eb519d8d139ef", size = 10033537, upload-time = "2026-01-27T00:58:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/4523b36f2ce69f92ccf783855a9e0ebbbd0f0bb5cdce6211ee1737159ed3/ty-0.0.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dc04384e874c5de4c5d743369c277c8aa73d1edea3c7fc646b2064b637db4db3", size = 10495910, upload-time = "2026-01-27T00:57:26.691Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/655beb51224d1bfd4f9ddc0bb209659bfe71ff141bcf05c418ab670698f0/ty-0.0.14-py3-none-win32.whl", hash = "sha256:b20e22cf54c66b3e37e87377635da412d9a552c9bf4ad9fc449fed8b2e19dad2", size = 9507626, upload-time = "2026-01-27T00:57:41.43Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d9/c569c9961760e20e0a4bc008eeb1415754564304fd53997a371b7cf3f864/ty-0.0.14-py3-none-win_amd64.whl", hash = "sha256:e312ff9475522d1a33186657fe74d1ec98e4a13e016d66f5758a452c90ff6409", size = 10437980, upload-time = "2026-01-27T00:57:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/186829654f5bfd9a028f6648e9caeb11271960a61de97484627d24443f91/ty-0.0.14-py3-none-win_arm64.whl", hash = "sha256:b6facdbe9b740cb2c15293a1d178e22ffc600653646452632541d01c36d5e378", size = 9885831, upload-time = "2026-01-27T00:57:49.747Z" }, ] [[package]] From d5ab1c44ff4fe8edda088032add27cb7f89b095c Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 27 Jan 2026 18:54:40 -0800 Subject: [PATCH 052/105] restore bout hooks --- frontend/app/main.tsx | 7 +++---- frontend/app/scoreboard.tsx | 6 +++--- frontend/components/bout-state-view.tsx | 2 +- .../components/intermission-state-view.tsx | 2 +- frontend/hooks/use-bout.ts | 19 +++++++------------ frontend/lib/game/bouts.ts | 2 +- 6 files changed, 16 insertions(+), 22 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 1c38a18d..e80e54bb 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -2,7 +2,7 @@ import BoutControlButtons from "@/components/bout-control-buttons"; import BoutStateView from "@/components/bout-state-view"; import TeamJamView from "@/components/team-jam-view"; import TeamView from "@/features/team-view/team-view"; -import { useAllBouts, useBout } from "@/hooks/use-bout"; +import { useAllBouts, useSuspenseBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; @@ -89,14 +89,13 @@ function Main() { } }, [allBouts]); - const { data: bout } = useBout(allBouts[0].uuid); + const { data: bout } = useSuspenseBout(allBouts[0].uuid); // Eagerly query the latest Jam and Timeout to avoid suspending void useJam(bout, ...bout.getLatestJamNum()); // Fetch Jam data - const jamIndex = bout.getActiveOrLatestJamIndex(); - const [periodNum, jamNum] = jamIndex; + const [periodNum, jamNum] = bout.getActiveOrLatestJamNum(); const { data: jam } = useSuspenseJam(bout, periodNum, jamNum); const timeout = new Timeout(); // FIXME diff --git a/frontend/app/scoreboard.tsx b/frontend/app/scoreboard.tsx index 9a9e9203..76baa903 100644 --- a/frontend/app/scoreboard.tsx +++ b/frontend/app/scoreboard.tsx @@ -1,6 +1,6 @@ import BoutStateView from "@/components/bout-state-view"; import TeamView from "@/features/team-view/team-view"; -import { useAllBouts, useBout } from "@/hooks/use-bout"; +import { useAllBouts, useSuspenseBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; @@ -45,7 +45,7 @@ function Scoreboard() { } }, [allBouts]); - const { data: bout } = useBout(allBouts[0].uuid); + const { data: bout } = useSuspenseBout(allBouts[0].uuid); // Eagerly query the latest Jam and Timeout to avoid suspending void useJam(bout, ...bout.getLatestJamNum()); @@ -53,7 +53,7 @@ function Scoreboard() { const { data: ruleset } = useSuspenseRuleset(bout); // Fetch Jam data - const jamIndex = bout.getActiveOrLatestJamIndex(); + const jamIndex = bout.getActiveOrLatestJamNum(); const [periodNum, jamNum] = jamIndex; const { data: jam } = useSuspenseJam(bout, periodNum, jamNum); diff --git a/frontend/components/bout-state-view.tsx b/frontend/components/bout-state-view.tsx index 6dfbed7f..19d74423 100644 --- a/frontend/components/bout-state-view.tsx +++ b/frontend/components/bout-state-view.tsx @@ -29,7 +29,7 @@ export default function BoutStateView({ ); } - let [periodNum, jamNum] = bout.getActiveOrLatestJamIndex(); + let [periodNum, jamNum] = bout.getActiveOrLatestJamNum(); if (periodNum >= 2) { // Overtime Jams should be considered a continuation of the second half periodNum = 1; diff --git a/frontend/components/intermission-state-view.tsx b/frontend/components/intermission-state-view.tsx index 843d6877..7c28e528 100644 --- a/frontend/components/intermission-state-view.tsx +++ b/frontend/components/intermission-state-view.tsx @@ -10,7 +10,7 @@ export default function IntermissionState({ bout, ...props }: IntermissionStateViewProps) { - const [periodNum] = bout.getActiveOrLatestJamIndex(); + const [periodNum] = bout.getActiveOrLatestJamNum(); let copy = "Starting Soon"; if (bout.isFinal) { diff --git a/frontend/hooks/use-bout.ts b/frontend/hooks/use-bout.ts index ba52e6fd..a1a19d87 100644 --- a/frontend/hooks/use-bout.ts +++ b/frontend/hooks/use-bout.ts @@ -1,9 +1,5 @@ import { Bout, createBout, getAllBouts, getBout } from "@/lib/game/bouts"; -import { - QueryOptions, - useMutation, - useSuspenseQuery, -} from "@tanstack/react-query"; +import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; export const useAllBouts = () => useSuspenseQuery({ @@ -11,14 +7,13 @@ export const useAllBouts = () => queryFn: () => getAllBouts(), }); -export const boutQueryOptions: (uuid: string) => QueryOptions = ( - uuid: string, -) => ({ - queryKey: Bout.generateKey(uuid), - queryFn: () => getBout(uuid), -}); - export const useBout = (uuid: string) => + useQuery({ + queryKey: Bout.generateKey(uuid), + queryFn: () => getBout(uuid), + }); + +export const useSuspenseBout = (uuid: string) => useSuspenseQuery({ queryKey: Bout.generateKey(uuid), queryFn: () => getBout(uuid), diff --git a/frontend/lib/game/bouts.ts b/frontend/lib/game/bouts.ts index 390a0157..55d954f7 100644 --- a/frontend/lib/game/bouts.ts +++ b/frontend/lib/game/bouts.ts @@ -106,7 +106,7 @@ export class Bout { return [periodNum, jamNum]; } - getActiveOrLatestJamIndex(): [number, number] { + getActiveOrLatestJamNum(): [number, number] { return this.getActiveJamNum() ?? this.getLatestJamNum(); } From bc0ffbd0263ca58e5ba27b390d2b672ece14caee Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 27 Jan 2026 19:53:45 -0800 Subject: [PATCH 053/105] fix series api call --- frontend/hooks/use-series.ts | 14 ++++++-------- frontend/lib/game/series.ts | 16 +++++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/hooks/use-series.ts b/frontend/hooks/use-series.ts index 77e88d41..ea063fbe 100644 --- a/frontend/hooks/use-series.ts +++ b/frontend/hooks/use-series.ts @@ -1,16 +1,14 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { getSeries, Series } from "../lib/game/series"; -export const useSuspenseSeries = (index: number) => +export const useSuspenseSeries = (uuid: string) => useSuspenseQuery({ - queryKey: ["series", "all"], - queryFn: () => getSeries(), - select: (data: Series[]) => data[index], + queryKey: Series.generateKey(uuid), + queryFn: () => getSeries(uuid), }); -export const useSeries = (index: number) => +export const useSeries = (uuid: string) => useQuery({ - queryKey: ["series", "all"], - queryFn: () => getSeries(), - select: (data: Series[]) => data[index], + queryKey: Series.generateKey(uuid), + queryFn: () => getSeries(uuid), }); diff --git a/frontend/lib/game/series.ts b/frontend/lib/game/series.ts index 46109f4f..b7cd1a16 100644 --- a/frontend/lib/game/series.ts +++ b/frontend/lib/game/series.ts @@ -1,20 +1,22 @@ import { CacheKey } from "@/types/ws"; import { localAPI } from "../requests"; -export async function getSeries(): Promise { - const data = await localAPI.get[]>("series"); +export async function getSeries(seriesUuid: string): Promise { + const data = await localAPI.get[]>("series", { + query: { seriesUuid }, + }); return data.map((series: Partial) => Object.assign(new Series(), series), ); } export class Series { - id: number; + uuid: string; name: string; - boutIds: number[]; - activeBoutId: number | null; + boutUuids: string[]; + activeBoutIndex: number | null; - static generateKey(): CacheKey { - return ["series", "all"]; + static generateKey(uuid: string): CacheKey { + return ["series", uuid]; } } From f29393692b73d96690d0d378ed67b742c7ff3396 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 27 Jan 2026 20:01:13 -0800 Subject: [PATCH 054/105] update api params --- frontend/lib/game/bouts.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/lib/game/bouts.ts b/frontend/lib/game/bouts.ts index 55d954f7..53815257 100644 --- a/frontend/lib/game/bouts.ts +++ b/frontend/lib/game/bouts.ts @@ -48,27 +48,29 @@ export class Bout { } async beginPeriod(): Promise { - await localAPI.post("bout/beginPeriod", { query: { boutId: this.uuid } }); + await localAPI.post("bout/beginPeriod", { query: { boutUuid: this.uuid } }); } async endPeriod(): Promise { - await localAPI.post("bout/endPeriod", { query: { boutId: this.uuid } }); + await localAPI.post("bout/endPeriod", { query: { boutUuid: this.uuid } }); } async startJam(): Promise { - await localAPI.post("bout/startJam", { query: { boutId: this.uuid } }); + await localAPI.post("bout/startJam", { query: { boutUuid: this.uuid } }); } async stopJam(): Promise { - await localAPI.post("bout/stopJam", { query: { boutId: this.uuid } }); + await localAPI.post("bout/stopJam", { query: { boutUuid: this.uuid } }); } async startTimeout(): Promise { - await localAPI.post("bout/startTimeout", { query: { boutId: this.uuid } }); + await localAPI.post("bout/startTimeout", { + query: { boutUuid: this.uuid }, + }); } async stopTimeout(): Promise { - await localAPI.post("bout/stopTimeout", { query: { boutId: this.uuid } }); + await localAPI.post("bout/stopTimeout", { query: { boutUuid: this.uuid } }); } getLatestJamNum(): [number, number] { From 438c858929263bca20112177e672f20692b062fb Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 27 Jan 2026 20:33:07 -0800 Subject: [PATCH 055/105] clarify suspense queries and add series hooks --- frontend/app/main.tsx | 21 ++++++++++----------- frontend/app/scoreboard.tsx | 4 ++-- frontend/hooks/use-bout.ts | 2 +- frontend/hooks/use-series.ts | 12 +++++++++--- frontend/lib/game/series.ts | 21 +++++++++++++++------ 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index e80e54bb..5f0cd13a 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -2,12 +2,14 @@ import BoutControlButtons from "@/components/bout-control-buttons"; import BoutStateView from "@/components/bout-state-view"; import TeamJamView from "@/components/team-jam-view"; import TeamView from "@/features/team-view/team-view"; -import { useAllBouts, useSuspenseBout } from "@/hooks/use-bout"; +import { useSuspenseBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; +import { useSuspenseAllSeries } from "@/hooks/use-series"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; import queryClient from "@/lib/cache"; -import { Bout, Team } from "@/lib/game/bouts"; +import { Team } from "@/lib/game/bouts"; +import { Series } from "@/lib/game/series"; import { Timeout } from "@/lib/game/timeouts"; import { redo, undo } from "@/lib/history"; import { BoutContext, JamContext, RulesetContext } from "@/utils/contexts"; @@ -22,7 +24,7 @@ import { import "@mantine/core/styles.css"; import { useDisclosure } from "@mantine/hooks"; import { QueryClientProvider } from "@tanstack/react-query"; -import { StrictMode, Suspense, useEffect } from "react"; +import { StrictMode, Suspense } from "react"; import { createRoot } from "react-dom/client"; import "./global.css"; @@ -81,15 +83,12 @@ export default function App() { function Main() { usePrefetchServerTime(); - const { data: allBouts } = useAllBouts(); + const { data: allSeries } = useSuspenseAllSeries(); - useEffect(() => { - for (const bout of allBouts) { - queryClient.setQueryData(Bout.generateKey(bout.uuid), bout); - } - }, [allBouts]); - - const { data: bout } = useSuspenseBout(allBouts[0].uuid); + const series: Series = allSeries[0]; + const { data: bout } = useSuspenseBout( + series.boutUuids[series.activeBoutIndex ?? series.boutUuids.length - 1], + ); // Eagerly query the latest Jam and Timeout to avoid suspending void useJam(bout, ...bout.getLatestJamNum()); diff --git a/frontend/app/scoreboard.tsx b/frontend/app/scoreboard.tsx index 76baa903..178abe1a 100644 --- a/frontend/app/scoreboard.tsx +++ b/frontend/app/scoreboard.tsx @@ -1,6 +1,6 @@ import BoutStateView from "@/components/bout-state-view"; import TeamView from "@/features/team-view/team-view"; -import { useAllBouts, useSuspenseBout } from "@/hooks/use-bout"; +import { useSuspenseAllBouts, useSuspenseBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; @@ -37,7 +37,7 @@ export default function App() { function Scoreboard() { usePrefetchServerTime(); - const { data: allBouts } = useAllBouts(); + const { data: allBouts } = useSuspenseAllBouts(); useEffect(() => { for (const bout of allBouts) { diff --git a/frontend/hooks/use-bout.ts b/frontend/hooks/use-bout.ts index a1a19d87..0546fe43 100644 --- a/frontend/hooks/use-bout.ts +++ b/frontend/hooks/use-bout.ts @@ -1,7 +1,7 @@ import { Bout, createBout, getAllBouts, getBout } from "@/lib/game/bouts"; import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; -export const useAllBouts = () => +export const useSuspenseAllBouts = () => useSuspenseQuery({ queryKey: Bout.generateKey(), queryFn: () => getAllBouts(), diff --git a/frontend/hooks/use-series.ts b/frontend/hooks/use-series.ts index ea063fbe..c8bc27c8 100644 --- a/frontend/hooks/use-series.ts +++ b/frontend/hooks/use-series.ts @@ -1,14 +1,20 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; -import { getSeries, Series } from "../lib/game/series"; +import { getAllSeries, getSeries, Series } from "@/lib/game/series"; + +export const useSuspenseAllSeries = () => + useSuspenseQuery({ + queryKey: Series.generateKey(), + queryFn: () => getAllSeries(), + }); export const useSuspenseSeries = (uuid: string) => - useSuspenseQuery({ + useSuspenseQuery({ queryKey: Series.generateKey(uuid), queryFn: () => getSeries(uuid), }); export const useSeries = (uuid: string) => - useQuery({ + useQuery({ queryKey: Series.generateKey(uuid), queryFn: () => getSeries(uuid), }); diff --git a/frontend/lib/game/series.ts b/frontend/lib/game/series.ts index b7cd1a16..ef3431db 100644 --- a/frontend/lib/game/series.ts +++ b/frontend/lib/game/series.ts @@ -1,22 +1,31 @@ import { CacheKey } from "@/types/ws"; import { localAPI } from "../requests"; -export async function getSeries(seriesUuid: string): Promise { - const data = await localAPI.get[]>("series", { - query: { seriesUuid }, - }); +export async function getAllSeries(): Promise { + const data = await localAPI.get[]>("series/allSeries"); return data.map((series: Partial) => Object.assign(new Series(), series), ); } +export async function getSeries(seriesUuid: string): Promise { + const data = await localAPI.get>("series", { + query: { seriesUuid }, + }); + return Object.assign(new Series(), data); +} + export class Series { uuid: string; name: string; boutUuids: string[]; activeBoutIndex: number | null; - static generateKey(uuid: string): CacheKey { - return ["series", uuid]; + static generateKey(uuid?: string): CacheKey { + const key: CacheKey = ["series"]; + if (uuid != undefined) { + key.push(uuid); + } + return key; } } From e7aedde56b310daa8d996c02be1c5a28b05f0c2f Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 27 Jan 2026 20:41:41 -0800 Subject: [PATCH 056/105] use bout getter strategy --- frontend/app/scoreboard.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/frontend/app/scoreboard.tsx b/frontend/app/scoreboard.tsx index 178abe1a..4eb32312 100644 --- a/frontend/app/scoreboard.tsx +++ b/frontend/app/scoreboard.tsx @@ -1,17 +1,19 @@ import BoutStateView from "@/components/bout-state-view"; import TeamView from "@/features/team-view/team-view"; -import { useSuspenseAllBouts, useSuspenseBout } from "@/hooks/use-bout"; +import { useSuspenseBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; +import { useSuspenseAllSeries } from "@/hooks/use-series"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; import { useSuspenseTimeout } from "@/hooks/use-timeout"; import queryClient from "@/lib/cache"; -import { Bout, Team } from "@/lib/game/bouts"; +import { Team } from "@/lib/game/bouts"; +import { Series } from "@/lib/game/series"; import FitScreen from "@fit-screen/react"; import { Center, Grid, MantineProvider, Stack } from "@mantine/core"; import "@mantine/core/styles.css"; import { QueryClientProvider } from "@tanstack/react-query"; -import { StrictMode, Suspense, useEffect } from "react"; +import { StrictMode, Suspense } from "react"; import { createRoot } from "react-dom/client"; import "./global.css"; @@ -37,15 +39,12 @@ export default function App() { function Scoreboard() { usePrefetchServerTime(); - const { data: allBouts } = useSuspenseAllBouts(); + const { data: allSeries } = useSuspenseAllSeries(); - useEffect(() => { - for (const bout of allBouts) { - queryClient.setQueryData(Bout.generateKey(bout.uuid), bout); - } - }, [allBouts]); - - const { data: bout } = useSuspenseBout(allBouts[0].uuid); + const series: Series = allSeries[0]; + const { data: bout } = useSuspenseBout( + series.boutUuids[series.activeBoutIndex ?? series.boutUuids.length - 1], + ); // Eagerly query the latest Jam and Timeout to avoid suspending void useJam(bout, ...bout.getLatestJamNum()); From 117259f59525d803bca8acc1d268da2ecacc5bdb Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 20:22:54 -0800 Subject: [PATCH 057/105] make arguments alias to camelCase --- backend/src/game/jams/router.py | 2 +- backend/src/game/teams/dependencies.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/game/jams/router.py b/backend/src/game/jams/router.py index 2f777d16..d04a3b56 100644 --- a/backend/src/game/jams/router.py +++ b/backend/src/game/jams/router.py @@ -49,7 +49,7 @@ async def set_lost( async def set_star_pass( jam: GetJam, team: GetTeam, - star_pass: Annotated[bool, Body()], + star_pass: Annotated[bool, Body(alias='starPass')], ) -> None: """Set a Star Pass for the specified Team of the specified Jam.""" await jam.set_star_pass(team, datetime.now(), star_pass) diff --git a/backend/src/game/teams/dependencies.py b/backend/src/game/teams/dependencies.py index 63980f23..731b5284 100644 --- a/backend/src/game/teams/dependencies.py +++ b/backend/src/game/teams/dependencies.py @@ -9,7 +9,9 @@ from .models import BaseTeam -async def _get_team(bout: GetBout, team_num: Annotated[int, Query()]) -> BaseTeam: +async def _get_team( + bout: GetBout, team_num: Annotated[int, Query(alias='teamNum')] +) -> BaseTeam: try: return bout.teams[team_num] except KeyError as e: From 76023b7d3cc967e2849996c9323d8e72b9630d39 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 20:23:21 -0800 Subject: [PATCH 058/105] update API to match backend --- frontend/components/team-jam-jammer-state.tsx | 13 ++-- frontend/hooks/use-jam.ts | 16 ++--- frontend/lib/game/jams.ts | 64 ++++++++----------- 3 files changed, 42 insertions(+), 51 deletions(-) diff --git a/frontend/components/team-jam-jammer-state.tsx b/frontend/components/team-jam-jammer-state.tsx index 0b2efc5b..8eb90b91 100644 --- a/frontend/components/team-jam-jammer-state.tsx +++ b/frontend/components/team-jam-jammer-state.tsx @@ -1,4 +1,3 @@ -import { useSetLead, useSetLost, useSetStarPass } from "@/hooks/use-jam"; import { TeamJam } from "@/lib/game/jams"; import { Checkbox, @@ -20,9 +19,9 @@ const checkBoxTheme = createTheme({ export default function TeamJamJammerState({ teamJam, }: JammerStatusButtonsProps) { - const setLead = useSetLead(teamJam); - const setLost = useSetLost(teamJam); - const setStarPass = useSetStarPass(teamJam); + // const setLead = useSetLead(teamJam); + // const setLost = useSetLost(teamJam); + // const setStarPass = useSetStarPass(teamJam); const lead = teamJam.events.some((tripEvent) => tripEvent.lead); const lost = teamJam.events.some((tripEvent) => tripEvent.lost); @@ -35,7 +34,7 @@ export default function TeamJamJammerState({ label="Lead" checked={lead} disabled={lost} - onClick={() => setLead.mutate(!lead)} + // onClick={() => setLead.mutate(!lead)} variant="outline" icon={({ ...others }) => } /> @@ -43,14 +42,14 @@ export default function TeamJamJammerState({ setLost.mutate(!lost)} + // onClick={() => setLost.mutate(!lost)} variant="outline" /> setStarPass.mutate(!starPass)} + // onClick={() => setStarPass.mutate(!starPass)} variant="outline" /> diff --git a/frontend/hooks/use-jam.ts b/frontend/hooks/use-jam.ts index 4e5d8e37..85eb4479 100644 --- a/frontend/hooks/use-jam.ts +++ b/frontend/hooks/use-jam.ts @@ -14,22 +14,22 @@ export const useJam = (bout: Bout, periodNum: number, jamNum: number) => queryFn: () => getJam(bout.uuid, periodNum, jamNum), }); -export const useAddTrip = (teamJam: TeamJam) => +export const useAddTrip = (jam: Jam, teamJam: TeamJam) => useMutation({ - mutationFn: (passes: number) => teamJam.addTrip(passes), + mutationFn: (passes: number) => jam.addTrip(teamJam, passes), }); -export const useSetLead = (teamJam: TeamJam) => +export const useSetLead = (jam: Jam, teamJam: TeamJam) => useMutation({ - mutationFn: (lead: boolean) => teamJam.setLead(lead), + mutationFn: (lead: boolean) => jam.setLead(teamJam, lead), }); -export const useSetLost = (teamJam: TeamJam) => +export const useSetLost = (jam: Jam, teamJam: TeamJam) => useMutation({ - mutationFn: (lost: boolean) => teamJam.setLost(lost), + mutationFn: (lost: boolean) => jam.setLost(teamJam, lost), }); -export const useSetStarPass = (teamJam: TeamJam) => +export const useSetStarPass = (jam: Jam, teamJam: TeamJam) => useMutation({ - mutationFn: (starPass: boolean) => teamJam.setStarPass(starPass), + mutationFn: (starPass: boolean) => jam.setStarPass(teamJam, starPass), }); diff --git a/frontend/lib/game/jams.ts b/frontend/lib/game/jams.ts index d3dfe817..16dae0d0 100644 --- a/frontend/lib/game/jams.ts +++ b/frontend/lib/game/jams.ts @@ -42,30 +42,50 @@ export class Jam { return this.hasStarted() && this.stopTimestamp == null; } - async addTrip(passes: number) { + async addTrip(teamJam: TeamJam, passes: number) { await localAPI.post("jam/addTrip", { - // query: { jamId: this.jamId, teamId: this.teamId }, + query: { + boutUuid: this.boutUuid, + period: this.period, + num: this.num, + teamNum: teamJam.teamNum, + }, body: passes, }); } - async setLead(lead: boolean) { + async setLead(teamJam: TeamJam, lead: boolean) { await localAPI.post("jam/setLead", { - // query: { jamId: this.jamId, teamId: this.teamId }, + query: { + boutUuid: this.boutUuid, + period: this.period, + num: this.num, + teamNum: teamJam.teamNum, + }, body: lead, }); } - async setLost(lost: boolean) { + async setLost(teamJam: TeamJam, lost: boolean) { await localAPI.post("jam/setLost", { - // query: { jamId: this.jamId, teamId: this.teamId }, + query: { + boutUuid: this.boutUuid, + period: this.period, + num: this.num, + teamNum: teamJam.teamNum, + }, body: lost, }); } - async setStarPass(starPass: boolean) { + async setStarPass(teamJam: TeamJam, starPass: boolean) { await localAPI.post("jam/setStarPass", { - // query: { jamId: this.jamId, teamId: this.teamId }, + query: { + boutUuid: this.boutUuid, + period: this.period, + num: this.num, + teamNum: teamJam.teamNum, + }, body: starPass, }); } @@ -74,34 +94,6 @@ export class Jam { export class TeamJam { teamNum: number; events: TripEvent[]; - - async addTrip(passes: number) { - await localAPI.post("jam/addTrip", { - // query: { jamId: this.jamId, teamId: this.teamId }, // FIXME - body: passes, - }); - } - - async setLead(lead: boolean) { - await localAPI.post("jam/setLead", { - // query: { jamId: this.jamId, teamId: this.teamId }, // FIXME - body: lead, - }); - } - - async setLost(lost: boolean) { - await localAPI.post("jam/setLost", { - // query: { jamId: this.jamId, teamId: this.teamId }, // FIXME - body: lost, - }); - } - - async setStarPass(starPass: boolean) { - await localAPI.post("jam/setStarPass", { - // query: { jamId: this.jamId, teamId: this.teamId }, // FIXME - body: starPass, - }); - } } export interface TripEvent { From 0b6c15dda25a2c10dcd13cc77cc3ee190ddc540a Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 20:25:15 -0800 Subject: [PATCH 059/105] comment out bad function calls --- frontend/components/add-trip-buttons.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/components/add-trip-buttons.tsx b/frontend/components/add-trip-buttons.tsx index 11a34c5b..89cee488 100644 --- a/frontend/components/add-trip-buttons.tsx +++ b/frontend/components/add-trip-buttons.tsx @@ -1,4 +1,3 @@ -import { useAddTrip } from "@/hooks/use-jam"; import { TeamJam } from "@/lib/game/jams"; import { Ruleset } from "@/lib/game/ruleset"; import { Button, Group, Stack } from "@mantine/core"; @@ -12,15 +11,21 @@ export default function AddTripButtons({ teamJam, ruleset, }: AddTripButtonsProps) { - const addTrip = useAddTrip(teamJam); + // const addTrip = useAddTrip(teamJam); const addTripButtons = teamJam.events.length == 0 ? ( <> - - @@ -30,7 +35,7 @@ export default function AddTripButtons({ From d30041d0dfae231c7c6c7e297800cf93a3dc6362 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 20:27:43 -0800 Subject: [PATCH 060/105] remove field from type --- frontend/lib/game/jams.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/lib/game/jams.ts b/frontend/lib/game/jams.ts index 16dae0d0..b14f8c3d 100644 --- a/frontend/lib/game/jams.ts +++ b/frontend/lib/game/jams.ts @@ -97,7 +97,6 @@ export class TeamJam { } export interface TripEvent { - id: number; timestamp: Date; lead: boolean; lost: boolean; From e3ea5dc9fe90490bf575214e45a6698b297d7cdc Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 20:53:38 -0800 Subject: [PATCH 061/105] organize features --- frontend/app/main.tsx | 7 +++---- frontend/app/scoreboard.tsx | 2 +- .../bout-control}/bout-control-buttons.tsx | 0 .../bout-control}/timeout-buttons.tsx | 0 .../bout-state-view}/bout-state-view.tsx | 2 +- .../bout-state-view}/intermission-state-view.tsx | 0 .../team-jam-vew}/add-trip-buttons.tsx | 0 .../team-jam-vew}/team-jam-jammer-state.tsx | 0 .../team-jam-vew}/team-jam-trips.tsx | 0 .../team-jam-vew}/team-jam-view.tsx | 2 +- frontend/features/team-view/team-view.tsx | 3 +-- 11 files changed, 7 insertions(+), 9 deletions(-) rename frontend/{components => features/bout-control}/bout-control-buttons.tsx (100%) rename frontend/{components => features/bout-control}/timeout-buttons.tsx (100%) rename frontend/{components => features/bout-state-view}/bout-state-view.tsx (95%) rename frontend/{components => features/bout-state-view}/intermission-state-view.tsx (100%) rename frontend/{components => features/team-jam-vew}/add-trip-buttons.tsx (100%) rename frontend/{components => features/team-jam-vew}/team-jam-jammer-state.tsx (100%) rename frontend/{components => features/team-jam-vew}/team-jam-trips.tsx (100%) rename frontend/{components => features/team-jam-vew}/team-jam-view.tsx (93%) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 5f0cd13a..e2f2a431 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -1,6 +1,6 @@ -import BoutControlButtons from "@/components/bout-control-buttons"; -import BoutStateView from "@/components/bout-state-view"; -import TeamJamView from "@/components/team-jam-view"; +import BoutControlButtons from "@/features/bout-control/bout-control-buttons"; +import BoutStateView from "@/features/bout-state-view/bout-state-view"; +import TeamJamView from "@/features/team-jam-vew/team-jam-view"; import TeamView from "@/features/team-view/team-view"; import { useSuspenseBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; @@ -112,7 +112,6 @@ function Main() { Date: Wed, 28 Jan 2026 20:54:12 -0800 Subject: [PATCH 062/105] remove arg --- frontend/app/scoreboard.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/app/scoreboard.tsx b/frontend/app/scoreboard.tsx index a01f22a4..dd2da904 100644 --- a/frontend/app/scoreboard.tsx +++ b/frontend/app/scoreboard.tsx @@ -65,12 +65,7 @@ function Scoreboard() { {bout.teams.map((team: Team, i: number) => ( - + ))} From 72d9b1cd1bf601f2691b987da9600ed830480ed4 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 21:18:29 -0800 Subject: [PATCH 063/105] update format --- frontend/app/main.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index e2f2a431..675771d9 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -116,7 +116,6 @@ function Main() { timeout={timeout} ruleset={ruleset} /> - ))} @@ -128,6 +127,17 @@ function Main() { activeTimeout={timeout} ruleset={ruleset} /> + + + {bout.teams.map((team: Team, i: number) => ( + + + + + + ))} + +
From 85905988e0334e5fb4a4e8e5d846116cdc668d78 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 21:27:30 -0800 Subject: [PATCH 064/105] use simplegrids --- frontend/app/main.tsx | 45 ++++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 675771d9..0575e624 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -12,13 +12,13 @@ import { Team } from "@/lib/game/bouts"; import { Series } from "@/lib/game/series"; import { Timeout } from "@/lib/game/timeouts"; import { redo, undo } from "@/lib/history"; -import { BoutContext, JamContext, RulesetContext } from "@/utils/contexts"; +import { BoutContext, RulesetContext } from "@/utils/contexts"; import { AppShell, Burger, Container, - Grid, MantineProvider, + SimpleGrid, Stack, } from "@mantine/core"; import "@mantine/core/styles.css"; @@ -106,38 +106,27 @@ function Main() { - - - {bout.teams.map((team: Team, i: number) => ( - - - - - - ))} - - + + {bout.teams.map((team: Team, i: number) => ( + + ))} + - - - {bout.teams.map((team: Team, i: number) => ( - - - - - - ))} - - + + {bout.teams.map((team: Team, i: number) => ( + + ))} + From d72182656a84b91b984eeb31811f9cc558bb2d00 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 22:08:54 -0800 Subject: [PATCH 065/105] refactor components for simplicity --- frontend/app/main.tsx | 50 +++++++++---------- frontend/app/scoreboard.tsx | 1 - .../components/extraordinary-state-clock.tsx | 18 +++---- .../bout-state-view/bout-state-view.tsx | 42 +++++++--------- 4 files changed, 50 insertions(+), 61 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 0575e624..1da44c67 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -16,7 +16,6 @@ import { BoutContext, RulesetContext } from "@/utils/contexts"; import { AppShell, Burger, - Container, MantineProvider, SimpleGrid, Stack, @@ -103,32 +102,29 @@ function Main() { return ( - - - - - {bout.teams.map((team: Team, i: number) => ( - - ))} - - - - {bout.teams.map((team: Team, i: number) => ( - - ))} - - - + + + + {bout.teams.map((team: Team, i: number) => ( + + ))} + + + + {bout.teams.map((team: Team, i: number) => ( + + ))} + + ); diff --git a/frontend/app/scoreboard.tsx b/frontend/app/scoreboard.tsx index dd2da904..0adc33f6 100644 --- a/frontend/app/scoreboard.tsx +++ b/frontend/app/scoreboard.tsx @@ -75,7 +75,6 @@ function Scoreboard() { bout={bout} activeOrLatestJam={jam} activeTimeout={timeout} - ruleset={ruleset} /> diff --git a/frontend/components/extraordinary-state-clock.tsx b/frontend/components/extraordinary-state-clock.tsx index 17f97cd5..bd804bcd 100644 --- a/frontend/components/extraordinary-state-clock.tsx +++ b/frontend/components/extraordinary-state-clock.tsx @@ -7,13 +7,13 @@ import { Text, TextProps } from "@mantine/core"; interface ExtraordinaryStateClockProps extends TextProps { bout: Bout; activeJam: Jam; - latestTimeout: Timeout | null; + activeTimeout: Timeout | null; } export default function ExtraordinaryStateClock({ bout, activeJam, - latestTimeout, + activeTimeout, ...props }: ExtraordinaryStateClockProps) { // Render non-Jam Bout states @@ -21,28 +21,28 @@ export default function ExtraordinaryStateClock({ let gameState = ""; if (bout.state === "lineup" && activeJam.stopTimestamp != null) { if ( - latestTimeout?.stopTimestamp != null && - latestTimeout.stopTimestamp > activeJam.stopTimestamp + activeTimeout?.stopTimestamp != null && + activeTimeout.stopTimestamp > activeJam.stopTimestamp ) { // Handle post-timeout lineup condition gameState = "Post-Timeout"; - gameStopTimestamp = latestTimeout.stopTimestamp; + gameStopTimestamp = activeTimeout.stopTimestamp; } else { // Handle standard lineup condition gameState = "Lineup"; gameStopTimestamp = activeJam.stopTimestamp; } } else if (bout.state === "timeout") { - if (latestTimeout?.teamIsOfficials) { + if (activeTimeout?.teamIsOfficials) { gameState = "Official Timeout"; - } else if (latestTimeout?.isReview) { + } else if (activeTimeout?.isReview) { gameState = "Official Review"; - } else if (latestTimeout?.teamNum != null) { + } else if (activeTimeout?.teamNum != null) { gameState = "Team Timeout"; } else { gameState = "Timeout"; } - gameStopTimestamp = latestTimeout!.startTimestamp; + gameStopTimestamp = activeTimeout!.startTimestamp; } return ( diff --git a/frontend/features/bout-state-view/bout-state-view.tsx b/frontend/features/bout-state-view/bout-state-view.tsx index 2744ec75..45957193 100644 --- a/frontend/features/bout-state-view/bout-state-view.tsx +++ b/frontend/features/bout-state-view/bout-state-view.tsx @@ -1,26 +1,26 @@ import ExtraordinaryStateClock from "@/components/extraordinary-state-clock"; -import IntermissionState from "@/features/bout-state-view/intermission-state-view"; import JamClock from "@/components/jam-clock"; import PeriodClock from "@/components/period-clock"; +import IntermissionState from "@/features/bout-state-view/intermission-state-view"; +import { useSuspenseRuleset } from "@/hooks/use-ruleset"; import { Bout } from "@/lib/game/bouts"; import { Jam } from "@/lib/game/jams"; -import { Ruleset } from "@/lib/game/ruleset"; import { Timeout } from "@/lib/game/timeouts"; -import { Center, Grid, Text } from "@mantine/core"; +import { Center, Group, Stack, Text } from "@mantine/core"; interface BoutStateViewProps { bout: Bout; activeOrLatestJam: Jam; activeTimeout: Timeout | null; - ruleset: Ruleset; } export default function BoutStateView({ bout, activeOrLatestJam, - activeTimeout: latestTimeout, - ruleset, + activeTimeout, }: BoutStateViewProps) { + const { data: ruleset } = useSuspenseRuleset(bout); + if (!bout.isRunning) { return (
@@ -37,20 +37,16 @@ export default function BoutStateView({ } return ( - - + +
-
-
P{periodNum + 1} J{jamNum + 1}
-
-
-
+ - -
- -
-
-
+
+ +
+ ); } From 640f6a91dc5a4612ad02500589c0255a52d04f4b Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 22:23:38 -0800 Subject: [PATCH 066/105] refactor components to reduce prop drilling --- frontend/app/main.tsx | 7 +------ frontend/app/scoreboard.tsx | 5 +---- frontend/components/timeout-bar.tsx | 11 ++++++----- frontend/features/team-view/team-view.tsx | 13 ++++++++----- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 1da44c67..e4dbab34 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -106,12 +106,7 @@ function Main() { {bout.teams.map((team: Team, i: number) => ( - + ))} {bout.teams.map((team: Team, i: number) => ( - + ))} diff --git a/frontend/components/timeout-bar.tsx b/frontend/components/timeout-bar.tsx index 21fb2b05..c7d7d65d 100644 --- a/frontend/components/timeout-bar.tsx +++ b/frontend/components/timeout-bar.tsx @@ -1,5 +1,4 @@ import { Team } from "@/lib/game/bouts"; -import { Ruleset } from "@/lib/game/ruleset"; import { Timeout } from "@/lib/game/timeouts"; import { Card, Center, Divider } from "@mantine/core"; import { IconCircleFilled } from "@tabler/icons-react"; @@ -8,7 +7,8 @@ import { twMerge } from "tailwind-merge"; interface TimeoutBarProps { team: Team; activeTimeout?: Timeout | null; - ruleset: Ruleset; + numTimeouts: number; + numReviews: number; size: number; } @@ -38,7 +38,8 @@ function TimeoutPip({ export default function TimeoutBar({ team, activeTimeout, - ruleset, + numTimeouts, + numReviews, size = 30, }: TimeoutBarProps) { if (!activeTimeout?.isRunning()) { @@ -48,7 +49,7 @@ export default function TimeoutBar({ return ( - {Array.from({ length: ruleset.numTimeouts }, (_, i) => ( + {Array.from({ length: numTimeouts }, (_, i) => (
- {Array.from({ length: ruleset.numReviews }, (_, i) => ( + {Array.from({ length: numReviews }, (_, i) => (
@@ -22,7 +24,8 @@ export default function TeamView({ team, timeout, ruleset }: TeamsViewProps) { From c82774672d8bad4faba721713060c68bb0eb62e2 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Wed, 28 Jan 2026 22:26:33 -0800 Subject: [PATCH 067/105] remove context --- frontend/app/main.tsx | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index e4dbab34..5a801f06 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -4,7 +4,6 @@ import TeamJamView from "@/features/team-jam-vew/team-jam-view"; import TeamView from "@/features/team-view/team-view"; import { useSuspenseBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; -import { useSuspenseRuleset } from "@/hooks/use-ruleset"; import { useSuspenseAllSeries } from "@/hooks/use-series"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; import queryClient from "@/lib/cache"; @@ -12,7 +11,7 @@ import { Team } from "@/lib/game/bouts"; import { Series } from "@/lib/game/series"; import { Timeout } from "@/lib/game/timeouts"; import { redo, undo } from "@/lib/history"; -import { BoutContext, RulesetContext } from "@/utils/contexts"; +import { BoutContext } from "@/utils/contexts"; import { AppShell, Burger, @@ -98,29 +97,26 @@ function Main() { const timeout = new Timeout(); // FIXME - const { data: ruleset } = useSuspenseRuleset(bout); return ( - - - - - {bout.teams.map((team: Team, i: number) => ( - - ))} - - - - {bout.teams.map((team: Team, i: number) => ( - - ))} - - - + + + + {bout.teams.map((team: Team, i: number) => ( + + ))} + + + + {bout.teams.map((team: Team, i: number) => ( + + ))} + + ); } From 20964fcb64c2347752f9d923047397ace97f24cc Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 16:43:39 -0800 Subject: [PATCH 068/105] fix typo --- backend/src/game/team_jams/models.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/backend/src/game/team_jams/models.py b/backend/src/game/team_jams/models.py index 4285d1c1..2447d8c5 100644 --- a/backend/src/game/team_jams/models.py +++ b/backend/src/game/team_jams/models.py @@ -88,7 +88,7 @@ def __init__(self, team: BaseTeam) -> None: @override async def get_parents(self) -> tuple[BaseSQLModel, ...]: - return (await self.get_team(), await self.get_jam()) + return (await self.get_team(), self.jam) async def get_team(self) -> BaseTeam: """Get the Team that owns this TeamJam. @@ -98,12 +98,3 @@ async def get_team(self) -> BaseTeam: """ return await self.awaitable_attrs._team - - async def get_jam(self) -> BaseJam: - """Get the Jam that owns this TeamJam. - - Returns: - BaseJam: the Jam that owns this TeamJam. - - """ - return await self.awaitable_attrs._jam From ddbf666afb628632efb699277e0c7d4199544c94 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 16:43:58 -0800 Subject: [PATCH 069/105] fix trip buttons --- .../team-jam-vew/add-trip-buttons.tsx | 42 +++++++++++-------- .../features/team-jam-vew/team-jam-view.tsx | 5 +-- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/frontend/features/team-jam-vew/add-trip-buttons.tsx b/frontend/features/team-jam-vew/add-trip-buttons.tsx index 89cee488..df2f5075 100644 --- a/frontend/features/team-jam-vew/add-trip-buttons.tsx +++ b/frontend/features/team-jam-vew/add-trip-buttons.tsx @@ -1,31 +1,37 @@ -import { TeamJam } from "@/lib/game/jams"; -import { Ruleset } from "@/lib/game/ruleset"; +import { useAddTrip } from "@/hooks/use-jam"; +import { useSuspenseRuleset } from "@/hooks/use-ruleset"; +import { Team } from "@/lib/game/bouts"; +import { Jam, TeamJam } from "@/lib/game/jams"; +import { BoutContext } from "@/utils/contexts"; import { Button, Group, Stack } from "@mantine/core"; +import { useContext } from "react"; interface AddTripButtonsProps { - teamJam: TeamJam; - ruleset: Ruleset; + jam: Jam; + team: Team; } -export default function AddTripButtons({ - teamJam, - ruleset, -}: AddTripButtonsProps) { - // const addTrip = useAddTrip(teamJam); +export default function AddTripButtons({ jam, team }: AddTripButtonsProps) { + const teamJam: TeamJam | undefined = jam.teamJams.find( + (teamJam: TeamJam) => teamJam.teamNum === team.num, + ); + if (teamJam == undefined) { + throw new Error("team jam not found"); + } + const bout = useContext(BoutContext); + if (bout == null) { + throw new Error("AddTripButtons must be inside a Bout context"); + } + const { data: ruleset } = useSuspenseRuleset(bout); + const addTrip = useAddTrip(jam, teamJam); const addTripButtons = teamJam.events.length == 0 ? ( <> - - @@ -35,7 +41,7 @@ export default function AddTripButtons({ diff --git a/frontend/features/team-jam-vew/team-jam-view.tsx b/frontend/features/team-jam-vew/team-jam-view.tsx index 7269fdf0..652fd4fa 100644 --- a/frontend/features/team-jam-vew/team-jam-view.tsx +++ b/frontend/features/team-jam-vew/team-jam-view.tsx @@ -1,9 +1,7 @@ import TeamJamTrips from "@/features/team-jam-vew/team-jam-trips"; import { Team } from "@/lib/game/bouts"; import { Jam, TeamJam } from "@/lib/game/jams"; -import { RulesetContext } from "@/utils/contexts"; import { Stack } from "@mantine/core"; -import { useContext } from "react"; import AddTripButtons from "./add-trip-buttons"; import TeamJamJammerState from "./team-jam-jammer-state"; @@ -19,12 +17,11 @@ export default function TeamJamView({ jam, team }: TeamJamViewProps) { if (teamJam == undefined) { throw new Error("team jam not found"); } - const ruleset = useContext(RulesetContext)!; return ( - + ); From f4d404147cffeecccb0113409d5530d7c4d6b154 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 17:00:15 -0800 Subject: [PATCH 070/105] fix buttons --- .../team-jam-vew/team-jam-jammer-state.tsx | 28 +++++++++++++------ .../features/team-jam-vew/team-jam-view.tsx | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/frontend/features/team-jam-vew/team-jam-jammer-state.tsx b/frontend/features/team-jam-vew/team-jam-jammer-state.tsx index 8eb90b91..afcd8ee0 100644 --- a/frontend/features/team-jam-vew/team-jam-jammer-state.tsx +++ b/frontend/features/team-jam-vew/team-jam-jammer-state.tsx @@ -1,4 +1,6 @@ -import { TeamJam } from "@/lib/game/jams"; +import { useSetLead, useSetLost, useSetStarPass } from "@/hooks/use-jam"; +import { Team } from "@/lib/game/bouts"; +import { Jam, TeamJam } from "@/lib/game/jams"; import { Checkbox, createTheme, @@ -9,7 +11,8 @@ import { import { IconStarFilled } from "@tabler/icons-react"; interface JammerStatusButtonsProps { - teamJam: TeamJam; + jam: Jam; + team: Team; } const checkBoxTheme = createTheme({ @@ -17,11 +20,18 @@ const checkBoxTheme = createTheme({ }); export default function TeamJamJammerState({ - teamJam, + jam, + team, }: JammerStatusButtonsProps) { - // const setLead = useSetLead(teamJam); - // const setLost = useSetLost(teamJam); - // const setStarPass = useSetStarPass(teamJam); + const teamJam: TeamJam | undefined = jam.teamJams.find( + (teamJam: TeamJam) => teamJam.teamNum === team.num, + ); + if (teamJam == undefined) { + throw new Error("team jam not found"); + } + const setLead = useSetLead(jam, teamJam); + const setLost = useSetLost(jam, teamJam); + const setStarPass = useSetStarPass(jam, teamJam); const lead = teamJam.events.some((tripEvent) => tripEvent.lead); const lost = teamJam.events.some((tripEvent) => tripEvent.lost); @@ -34,7 +44,7 @@ export default function TeamJamJammerState({ label="Lead" checked={lead} disabled={lost} - // onClick={() => setLead.mutate(!lead)} + onClick={() => setLead.mutate(!lead)} variant="outline" icon={({ ...others }) => } /> @@ -42,14 +52,14 @@ export default function TeamJamJammerState({ setLost.mutate(!lost)} + onClick={() => setLost.mutate(!lost)} variant="outline" /> setStarPass.mutate(!starPass)} + onClick={() => setStarPass.mutate(!starPass)} variant="outline" /> diff --git a/frontend/features/team-jam-vew/team-jam-view.tsx b/frontend/features/team-jam-vew/team-jam-view.tsx index 652fd4fa..c443a155 100644 --- a/frontend/features/team-jam-vew/team-jam-view.tsx +++ b/frontend/features/team-jam-vew/team-jam-view.tsx @@ -20,7 +20,7 @@ export default function TeamJamView({ jam, team }: TeamJamViewProps) { return ( - + From 3879ddf4d1f423273c97389a01787aa327f9d773 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 17:12:53 -0800 Subject: [PATCH 071/105] add more checkbox logic --- .../team-jam-vew/team-jam-jammer-state.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/features/team-jam-vew/team-jam-jammer-state.tsx b/frontend/features/team-jam-vew/team-jam-jammer-state.tsx index afcd8ee0..5ce48648 100644 --- a/frontend/features/team-jam-vew/team-jam-jammer-state.tsx +++ b/frontend/features/team-jam-vew/team-jam-jammer-state.tsx @@ -37,13 +37,32 @@ export default function TeamJamJammerState({ const lost = teamJam.events.some((tripEvent) => tripEvent.lost); const starPass = teamJam.events.some((tripEvent) => tripEvent.starPass); + // The Lead checkbox should be disabled if another team has lead + let isLeadEligible = true; + if (!lost) { + for (const tj of jam.teamJams) { + if (tj == teamJam) { + continue; + } + for (const event of tj.events) { + if (event.lead) { + isLeadEligible = false; + break; + } + } + if (!isLeadEligible) { + break; + } + } + } + return ( setLead.mutate(!lead)} variant="outline" icon={({ ...others }) => } From 04ef06da31857e618b628c56912b670f070647ac Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 17:14:51 -0800 Subject: [PATCH 072/105] remove ruleset context --- frontend/utils/contexts.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/utils/contexts.ts b/frontend/utils/contexts.ts index 568ec9fb..cc3eac94 100644 --- a/frontend/utils/contexts.ts +++ b/frontend/utils/contexts.ts @@ -1,10 +1,7 @@ import { Bout } from "@/lib/game/bouts"; import { Jam } from "@/lib/game/jams"; -import { Ruleset } from "@/lib/game/ruleset"; import { createContext } from "react"; export const BoutContext = createContext(null); export const JamContext = createContext(null); - -export const RulesetContext = createContext(null); From 30fbda05314993b40cf7b168d0023147e89c5b7f Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 17:55:03 -0800 Subject: [PATCH 073/105] consolidate feature logic --- frontend/app/main.tsx | 2 +- frontend/app/scoreboard.tsx | 2 +- frontend/components/timeout-bar.tsx | 33 +++++++---------------- frontend/features/team-view/team-view.tsx | 29 +++++++++++++++----- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 5a801f06..ba23f2af 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -103,7 +103,7 @@ function Main() { {bout.teams.map((team: Team, i: number) => ( - + ))} {bout.teams.map((team: Team, i: number) => ( - + ))} diff --git a/frontend/components/timeout-bar.tsx b/frontend/components/timeout-bar.tsx index c7d7d65d..a6dd3653 100644 --- a/frontend/components/timeout-bar.tsx +++ b/frontend/components/timeout-bar.tsx @@ -1,14 +1,13 @@ -import { Team } from "@/lib/game/bouts"; -import { Timeout } from "@/lib/game/timeouts"; import { Card, Center, Divider } from "@mantine/core"; import { IconCircleFilled } from "@tabler/icons-react"; import { twMerge } from "tailwind-merge"; interface TimeoutBarProps { - team: Team; - activeTimeout?: Timeout | null; + activeType?: "timeout" | "review"; numTimeouts: number; + timeoutsRemaining: number; numReviews: number; + reviewsRemaining: number; size: number; } @@ -36,17 +35,13 @@ function TimeoutPip({ } export default function TimeoutBar({ - team, - activeTimeout, numTimeouts, numReviews, + activeType, + timeoutsRemaining, + reviewsRemaining, size = 30, }: TimeoutBarProps) { - if (!activeTimeout?.isRunning()) { - // There is no active Timeout - activeTimeout = null; - } - return ( {Array.from({ length: numTimeouts }, (_, i) => ( @@ -54,12 +49,8 @@ export default function TimeoutBar({
= team.timeoutsRemaining} - active={ - i == team.timeoutsRemaining - 1 && - activeTimeout?.teamNum == team.num && - !activeTimeout?.isReview - } + invisible={i >= timeoutsRemaining} + active={i == timeoutsRemaining - 1 && activeType == "timeout"} />
@@ -72,12 +63,8 @@ export default function TimeoutBar({
= team.reviewsRemaining} - active={ - i == team.reviewsRemaining - 1 && - activeTimeout?.teamNum == team.num && - activeTimeout?.isReview - } + invisible={i >= reviewsRemaining} + active={i == reviewsRemaining - 1 && activeType == "review"} />
diff --git a/frontend/features/team-view/team-view.tsx b/frontend/features/team-view/team-view.tsx index a5aa26e5..04d31be8 100644 --- a/frontend/features/team-view/team-view.tsx +++ b/frontend/features/team-view/team-view.tsx @@ -1,18 +1,28 @@ import TimeoutBar from "@/components/timeout-bar"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; +import { useTimeout } from "@/hooks/use-timeout"; import { Bout, Team } from "@/lib/game/bouts"; -import { Timeout } from "@/lib/game/timeouts"; +import { BoutContext } from "@/utils/contexts"; import { Center, Grid, Group, Stack, Text, Title } from "@mantine/core"; +import { useContext } from "react"; interface TeamsViewProps { - bout: Bout; team: Team; - timeout: Timeout; } -export default function TeamView({ bout, team, timeout }: TeamsViewProps) { +export default function TeamView({ team }: TeamsViewProps) { + const bout: Bout | null = useContext(BoutContext); + if (bout == null) { + throw new Error("TeamView can only be used in a BoutContext"); + } const { data: ruleset } = useSuspenseRuleset(bout); + const { + data: timeout, + isPending, + isError, + } = useTimeout(bout, bout.timeoutCount - 1); + return (
@@ -22,10 +32,17 @@ export default function TeamView({ bout, team, timeout }: TeamsViewProps) {
From 4c1b61df11872b2918baf6e8969ff585c238fdf3 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 18:04:46 -0800 Subject: [PATCH 074/105] consolidate logic --- frontend/app/main.tsx | 6 +--- .../bout-state-view/bout-state-view.tsx | 30 +++++++++---------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index ba23f2af..54517215 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -106,11 +106,7 @@ function Main() { ))} - + {bout.teams.map((team: Team, i: number) => ( diff --git a/frontend/features/bout-state-view/bout-state-view.tsx b/frontend/features/bout-state-view/bout-state-view.tsx index 45957193..3a4facd5 100644 --- a/frontend/features/bout-state-view/bout-state-view.tsx +++ b/frontend/features/bout-state-view/bout-state-view.tsx @@ -2,25 +2,28 @@ import ExtraordinaryStateClock from "@/components/extraordinary-state-clock"; import JamClock from "@/components/jam-clock"; import PeriodClock from "@/components/period-clock"; import IntermissionState from "@/features/bout-state-view/intermission-state-view"; +import { useSuspenseJam } from "@/hooks/use-jam"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; -import { Bout } from "@/lib/game/bouts"; -import { Jam } from "@/lib/game/jams"; import { Timeout } from "@/lib/game/timeouts"; +import { BoutContext } from "@/utils/contexts"; import { Center, Group, Stack, Text } from "@mantine/core"; +import { useContext } from "react"; interface BoutStateViewProps { - bout: Bout; - activeOrLatestJam: Jam; activeTimeout: Timeout | null; } -export default function BoutStateView({ - bout, - activeOrLatestJam, - activeTimeout, -}: BoutStateViewProps) { +export default function BoutStateView({ activeTimeout }: BoutStateViewProps) { + const bout = useContext(BoutContext); + if (bout == null) { + throw new Error("AddTripButtons must be inside a Bout context"); + } const { data: ruleset } = useSuspenseRuleset(bout); + // Fetch Jam data + let [periodNum, jamNum] = bout.getActiveOrLatestJamNum(); + const { data: jam } = useSuspenseJam(bout, periodNum, jamNum); + if (!bout.isRunning) { return (
@@ -29,7 +32,6 @@ export default function BoutStateView({ ); } - let [periodNum, jamNum] = bout.getActiveOrLatestJamNum(); if (periodNum >= 2) { // Overtime Jams should be considered a continuation of the second half periodNum = 1; @@ -48,11 +50,7 @@ export default function BoutStateView({
- +
@@ -60,7 +58,7 @@ export default function BoutStateView({
From f4d62cce407437a6056999b920c64b466ca5c0fa Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 18:15:49 -0800 Subject: [PATCH 075/105] remove args --- frontend/app/scoreboard.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/app/scoreboard.tsx b/frontend/app/scoreboard.tsx index c75f9f82..27c9a58c 100644 --- a/frontend/app/scoreboard.tsx +++ b/frontend/app/scoreboard.tsx @@ -1,7 +1,7 @@ import BoutStateView from "@/features/bout-state-view/bout-state-view"; import TeamView from "@/features/team-view/team-view"; import { useSuspenseBout } from "@/hooks/use-bout"; -import { useJam, useSuspenseJam } from "@/hooks/use-jam"; +import { useJam } from "@/hooks/use-jam"; import { useSuspenseAllSeries } from "@/hooks/use-series"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; import { useSuspenseTimeout } from "@/hooks/use-timeout"; @@ -48,11 +48,6 @@ function Scoreboard() { // Eagerly query the latest Jam and Timeout to avoid suspending void useJam(bout, ...bout.getLatestJamNum()); - // Fetch Jam data - const jamIndex = bout.getActiveOrLatestJamNum(); - const [periodNum, jamNum] = jamIndex; - const { data: jam } = useSuspenseJam(bout, periodNum, jamNum); - // Fetch Timeout data const timeoutIndex = bout.getActiveOrLatestTimeoutIndex(); const { data: timeout } = useSuspenseTimeout(bout, timeoutIndex); @@ -68,11 +63,7 @@ function Scoreboard() { {/* TODO: Lead Jam Status */}
- +
); From 867bd5429bdbcd6f9e04542e363f791c34f98dae Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 18:16:05 -0800 Subject: [PATCH 076/105] move component to feature --- frontend/features/bout-state-view/bout-state-view.tsx | 2 +- .../bout-state-view}/extraordinary-state-clock.tsx | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename frontend/{components => features/bout-state-view}/extraordinary-state-clock.tsx (100%) diff --git a/frontend/features/bout-state-view/bout-state-view.tsx b/frontend/features/bout-state-view/bout-state-view.tsx index 3a4facd5..1c5a8450 100644 --- a/frontend/features/bout-state-view/bout-state-view.tsx +++ b/frontend/features/bout-state-view/bout-state-view.tsx @@ -1,4 +1,4 @@ -import ExtraordinaryStateClock from "@/components/extraordinary-state-clock"; +import ExtraordinaryStateClock from "@/features/bout-state-view/extraordinary-state-clock"; import JamClock from "@/components/jam-clock"; import PeriodClock from "@/components/period-clock"; import IntermissionState from "@/features/bout-state-view/intermission-state-view"; diff --git a/frontend/components/extraordinary-state-clock.tsx b/frontend/features/bout-state-view/extraordinary-state-clock.tsx similarity index 100% rename from frontend/components/extraordinary-state-clock.tsx rename to frontend/features/bout-state-view/extraordinary-state-clock.tsx From b43a44dbd4082dea2dddf25a4fa854ab9e92471b Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 19:20:29 -0800 Subject: [PATCH 077/105] allow custom timeout queries --- frontend/app/main.tsx | 5 +---- frontend/app/scoreboard.tsx | 7 +------ .../bout-state-view/bout-state-view.tsx | 16 +++++++++------ frontend/hooks/use-timeout.ts | 20 ++++++++++++------- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 54517215..5c6ac93b 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -9,7 +9,6 @@ import { usePrefetchServerTime } from "@/hooks/use-server-time"; import queryClient from "@/lib/cache"; import { Team } from "@/lib/game/bouts"; import { Series } from "@/lib/game/series"; -import { Timeout } from "@/lib/game/timeouts"; import { redo, undo } from "@/lib/history"; import { BoutContext } from "@/utils/contexts"; import { @@ -95,8 +94,6 @@ function Main() { const [periodNum, jamNum] = bout.getActiveOrLatestJamNum(); const { data: jam } = useSuspenseJam(bout, periodNum, jamNum); - const timeout = new Timeout(); // FIXME - return ( @@ -106,7 +103,7 @@ function Main() { ))} - + {bout.teams.map((team: Team, i: number) => ( diff --git a/frontend/app/scoreboard.tsx b/frontend/app/scoreboard.tsx index 27c9a58c..acfe0e83 100644 --- a/frontend/app/scoreboard.tsx +++ b/frontend/app/scoreboard.tsx @@ -4,7 +4,6 @@ import { useSuspenseBout } from "@/hooks/use-bout"; import { useJam } from "@/hooks/use-jam"; import { useSuspenseAllSeries } from "@/hooks/use-series"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; -import { useSuspenseTimeout } from "@/hooks/use-timeout"; import queryClient from "@/lib/cache"; import { Team } from "@/lib/game/bouts"; import { Series } from "@/lib/game/series"; @@ -48,10 +47,6 @@ function Scoreboard() { // Eagerly query the latest Jam and Timeout to avoid suspending void useJam(bout, ...bout.getLatestJamNum()); - // Fetch Timeout data - const timeoutIndex = bout.getActiveOrLatestTimeoutIndex(); - const { data: timeout } = useSuspenseTimeout(bout, timeoutIndex); - return ( @@ -63,7 +58,7 @@ function Scoreboard() { {/* TODO: Lead Jam Status */}
- +
); diff --git a/frontend/features/bout-state-view/bout-state-view.tsx b/frontend/features/bout-state-view/bout-state-view.tsx index 1c5a8450..fd8abed4 100644 --- a/frontend/features/bout-state-view/bout-state-view.tsx +++ b/frontend/features/bout-state-view/bout-state-view.tsx @@ -8,18 +8,22 @@ import { Timeout } from "@/lib/game/timeouts"; import { BoutContext } from "@/utils/contexts"; import { Center, Group, Stack, Text } from "@mantine/core"; import { useContext } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { timeoutQueryOptions } from "@/hooks/use-timeout"; -interface BoutStateViewProps { - activeTimeout: Timeout | null; -} - -export default function BoutStateView({ activeTimeout }: BoutStateViewProps) { +export default function BoutStateView() { const bout = useContext(BoutContext); if (bout == null) { throw new Error("AddTripButtons must be inside a Bout context"); } const { data: ruleset } = useSuspenseRuleset(bout); + const { data: timeout } = useQuery({ + ...timeoutQueryOptions(bout, bout.timeoutCount - 1), + enabled: bout.timeoutCount > 0, + placeholderData: new Timeout(), + }); + // Fetch Jam data let [periodNum, jamNum] = bout.getActiveOrLatestJamNum(); const { data: jam } = useSuspenseJam(bout, periodNum, jamNum); @@ -59,7 +63,7 @@ export default function BoutStateView({ activeTimeout }: BoutStateViewProps) { size="24pt" bout={bout} activeJam={jam} - activeTimeout={activeTimeout} + activeTimeout={timeout!} />
diff --git a/frontend/hooks/use-timeout.ts b/frontend/hooks/use-timeout.ts index a23d8133..d20d5206 100644 --- a/frontend/hooks/use-timeout.ts +++ b/frontend/hooks/use-timeout.ts @@ -1,18 +1,24 @@ import { Bout } from "@/lib/game/bouts"; import { getTimeout, Timeout } from "@/lib/game/timeouts"; -import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { + queryOptions, + useMutation, + useQuery, + useSuspenseQuery, +} from "@tanstack/react-query"; -export const useTimeout = (bout: Bout, num: number) => - useQuery({ +export function timeoutQueryOptions(bout: Bout, num: number) { + return queryOptions({ queryKey: Timeout.generateKey(bout.uuid, num), queryFn: () => getTimeout(bout.uuid, num), }); +} + +export const useTimeout = (bout: Bout, num: number) => + useQuery(timeoutQueryOptions(bout, num)); export const useSuspenseTimeout = (bout: Bout, num: number) => - useSuspenseQuery({ - queryKey: Timeout.generateKey(bout.uuid, num), - queryFn: () => getTimeout(bout.uuid, num), - }); + useSuspenseQuery(timeoutQueryOptions(bout, num)); export const useSetType = (timeout: Timeout) => useMutation({ From f7f91840a2815e0e9e0e3b68732009646764f43e Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 19:35:01 -0800 Subject: [PATCH 078/105] use custom queries --- .../bout-state-view/bout-state-view.tsx | 6 +++--- frontend/features/team-view/team-view.tsx | 20 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/features/bout-state-view/bout-state-view.tsx b/frontend/features/bout-state-view/bout-state-view.tsx index fd8abed4..1e973825 100644 --- a/frontend/features/bout-state-view/bout-state-view.tsx +++ b/frontend/features/bout-state-view/bout-state-view.tsx @@ -1,15 +1,15 @@ -import ExtraordinaryStateClock from "@/features/bout-state-view/extraordinary-state-clock"; import JamClock from "@/components/jam-clock"; import PeriodClock from "@/components/period-clock"; +import ExtraordinaryStateClock from "@/features/bout-state-view/extraordinary-state-clock"; import IntermissionState from "@/features/bout-state-view/intermission-state-view"; import { useSuspenseJam } from "@/hooks/use-jam"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; +import { timeoutQueryOptions } from "@/hooks/use-timeout"; import { Timeout } from "@/lib/game/timeouts"; import { BoutContext } from "@/utils/contexts"; import { Center, Group, Stack, Text } from "@mantine/core"; -import { useContext } from "react"; import { useQuery } from "@tanstack/react-query"; -import { timeoutQueryOptions } from "@/hooks/use-timeout"; +import { useContext } from "react"; export default function BoutStateView() { const bout = useContext(BoutContext); diff --git a/frontend/features/team-view/team-view.tsx b/frontend/features/team-view/team-view.tsx index 04d31be8..d884c997 100644 --- a/frontend/features/team-view/team-view.tsx +++ b/frontend/features/team-view/team-view.tsx @@ -1,9 +1,11 @@ import TimeoutBar from "@/components/timeout-bar"; import { useSuspenseRuleset } from "@/hooks/use-ruleset"; -import { useTimeout } from "@/hooks/use-timeout"; +import { timeoutQueryOptions } from "@/hooks/use-timeout"; import { Bout, Team } from "@/lib/game/bouts"; +import { Timeout } from "@/lib/game/timeouts"; import { BoutContext } from "@/utils/contexts"; import { Center, Grid, Group, Stack, Text, Title } from "@mantine/core"; +import { useQuery } from "@tanstack/react-query"; import { useContext } from "react"; interface TeamsViewProps { @@ -17,11 +19,11 @@ export default function TeamView({ team }: TeamsViewProps) { } const { data: ruleset } = useSuspenseRuleset(bout); - const { - data: timeout, - isPending, - isError, - } = useTimeout(bout, bout.timeoutCount - 1); + const { data: timeout, isPending } = useQuery({ + ...timeoutQueryOptions(bout, bout.timeoutCount - 1), + enabled: bout.timeoutCount > 0, + placeholderData: new Timeout(), + }); return ( @@ -33,11 +35,7 @@ export default function TeamView({ team }: TeamsViewProps) { Date: Sun, 1 Feb 2026 19:41:12 -0800 Subject: [PATCH 079/105] fix field name --- backend/src/game/timeouts/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/game/timeouts/models.py b/backend/src/game/timeouts/models.py index 5875addb..66593a14 100644 --- a/backend/src/game/timeouts/models.py +++ b/backend/src/game/timeouts/models.py @@ -95,7 +95,7 @@ def __init__(self, jam: BaseJam, num: int) -> None: num (int): the unique Timeout number associated with this Bout. """ - super().__init__(_jam=jam, num=num) + super().__init__(jam=jam, num=num) @override async def cache_key(self) -> CacheKey: From c9eea3633bcf4792b26d1eaa952820c5f5707e27 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 19:45:41 -0800 Subject: [PATCH 080/105] correct blink behavior --- frontend/features/team-view/team-view.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/features/team-view/team-view.tsx b/frontend/features/team-view/team-view.tsx index d884c997..3c99e3f8 100644 --- a/frontend/features/team-view/team-view.tsx +++ b/frontend/features/team-view/team-view.tsx @@ -19,12 +19,19 @@ export default function TeamView({ team }: TeamsViewProps) { } const { data: ruleset } = useSuspenseRuleset(bout); - const { data: timeout, isPending } = useQuery({ + const { data: timeout } = useQuery({ ...timeoutQueryOptions(bout, bout.timeoutCount - 1), enabled: bout.timeoutCount > 0, - placeholderData: new Timeout(), + placeholderData: undefined, }); + const timeoutType: "review" | "timeout" | undefined = + timeout == undefined || timeout.teamIsOfficials + ? undefined + : timeout.isReview + ? "review" + : "timeout"; + return (
@@ -34,9 +41,7 @@ export default function TeamView({ team }: TeamsViewProps) {
Date: Sun, 1 Feb 2026 19:52:41 -0800 Subject: [PATCH 081/105] update mutation methods --- frontend/lib/game/timeouts.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/lib/game/timeouts.ts b/frontend/lib/game/timeouts.ts index 3f01fe32..bad5c328 100644 --- a/frontend/lib/game/timeouts.ts +++ b/frontend/lib/game/timeouts.ts @@ -54,21 +54,21 @@ export class Timeout { async setType(type: "timeout" | "review"): Promise { await localAPI.post("timeout/type", { - query: { timeoutId: this.uuid }, + query: { boutUuid: this.boutUuid, num: this.num }, body: JSON.stringify(type), }); } async setTeam(team: number | null): Promise { await localAPI.post("timeout/team", { - query: { timeoutId: this.uuid }, + query: { boutUuid: this.boutUuid, num: this.num }, body: team, }); } async setRetained(isRetained: boolean): Promise { await localAPI.post("timeout/retained", { - query: { timeoutId: this.uuid }, + query: { boutUuid: this.boutUuid, num: this.num }, body: isRetained, }); } From 107ee54785ebc3a0ef9d332afe3227b9595f02ab Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 20:30:04 -0800 Subject: [PATCH 082/105] cleanup setTeam endpoint --- backend/src/game/timeouts/router.py | 13 +++++++++---- .../features/bout-control/bout-control-buttons.tsx | 13 +++++++++---- frontend/features/bout-control/timeout-buttons.tsx | 4 ++-- frontend/hooks/use-timeout.ts | 2 +- frontend/lib/game/timeouts.ts | 6 +++--- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/backend/src/game/timeouts/router.py b/backend/src/game/timeouts/router.py index b132455b..89b11c82 100644 --- a/backend/src/game/timeouts/router.py +++ b/backend/src/game/timeouts/router.py @@ -1,13 +1,15 @@ """FastAPI routes associated with Timeouts.""" -from typing import Annotated, Final, Literal +from typing import TYPE_CHECKING, Annotated, Final, Literal from fastapi import APIRouter, Body -from game.teams.dependencies import GetTeam from .dependencies import GetTimeout, _get_timeout from .schemas import TimeoutSchema +if TYPE_CHECKING: + from game.bouts.models import BaseBout + TIMEOUTS_TAG = 'Timeouts' router: Final[APIRouter] = APIRouter(prefix='/timeout') @@ -26,9 +28,12 @@ async def set_type( @router.post('/team', tags=[TIMEOUTS_TAG]) -async def set_team(timeout: GetTimeout, team: GetTeam) -> None: +async def set_team( + timeout: GetTimeout, team_num: Annotated[int | None, Body()] +) -> None: """Set the calling Team of the specified Timeout.""" - timeout.set_team(team) + bout: BaseBout = await timeout.get_bout() + timeout.set_team(bout.teams[team_num] if team_num is not None else None) pass diff --git a/frontend/features/bout-control/bout-control-buttons.tsx b/frontend/features/bout-control/bout-control-buttons.tsx index dc639518..f25ee89d 100644 --- a/frontend/features/bout-control/bout-control-buttons.tsx +++ b/frontend/features/bout-control/bout-control-buttons.tsx @@ -7,10 +7,12 @@ import { useStopTimeout, } from "@/hooks/use-bout"; import { useRedo, useUndo } from "@/hooks/use-history"; -import { useSuspenseTimeout } from "@/hooks/use-timeout"; +import { timeoutQueryOptions } from "@/hooks/use-timeout"; import { Bout } from "@/lib/game/bouts"; +import { Timeout } from "@/lib/game/timeouts"; import { ActionIcon, Button, Divider, Grid, Group } from "@mantine/core"; import { IconArrowBackUp, IconArrowForwardUp } from "@tabler/icons-react"; +import { useQuery } from "@tanstack/react-query"; import TimeoutButtons from "./timeout-buttons"; interface MainControlProps { @@ -143,15 +145,18 @@ function TimeoutControlButtons({ bout }: MainControlProps) { const stopTimeout = useStopTimeout(bout); const startJam = useStartJam(bout); - // FIXME: Remove this hook? - const { data } = useSuspenseTimeout(bout, bout.getActiveTimeoutNum()!); + const { data } = useQuery({ + ...timeoutQueryOptions(bout, bout.timeoutCount - 1), + enabled: bout.timeoutCount > 0, + placeholderData: new Timeout(), + }); return ( <> - + ); } diff --git a/frontend/features/bout-control/timeout-buttons.tsx b/frontend/features/bout-control/timeout-buttons.tsx index 468639b2..5f386ae7 100644 --- a/frontend/features/bout-control/timeout-buttons.tsx +++ b/frontend/features/bout-control/timeout-buttons.tsx @@ -55,8 +55,8 @@ export default function TimeoutButtons({ : "" : String(timeout.teamNum) } - onChange={(teamId) => - setTeam.mutate(teamId == String(NaN) ? null : Number(teamId)) + onChange={(teamNum) => + setTeam.mutate(teamNum == String(NaN) ? null : Number(teamNum)) } /> diff --git a/frontend/hooks/use-timeout.ts b/frontend/hooks/use-timeout.ts index d20d5206..897d8c5d 100644 --- a/frontend/hooks/use-timeout.ts +++ b/frontend/hooks/use-timeout.ts @@ -27,7 +27,7 @@ export const useSetType = (timeout: Timeout) => export const useSetTeam = (timeout: Timeout) => useMutation({ - mutationFn: (teamId: number | null) => timeout.setTeam(teamId), + mutationFn: (teamNum: number | null) => timeout.setTeam(teamNum), }); export const useSetRetained = (timeout: Timeout) => diff --git a/frontend/lib/game/timeouts.ts b/frontend/lib/game/timeouts.ts index bad5c328..48e67a67 100644 --- a/frontend/lib/game/timeouts.ts +++ b/frontend/lib/game/timeouts.ts @@ -12,7 +12,7 @@ export async function getTimeout( } export default class Clock { - id: number; + id: number; // FIXME: remove this field startTimestamp: Date | null; elapsed: number; @@ -59,10 +59,10 @@ export class Timeout { }); } - async setTeam(team: number | null): Promise { + async setTeam(teamNum: number | null): Promise { await localAPI.post("timeout/team", { query: { boutUuid: this.boutUuid, num: this.num }, - body: team, + body: JSON.stringify(teamNum), // FIXME: this doesn't work when null }); } From a4c6c4f58e73e195f415a45a31a43fe5dba573a2 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Sun, 1 Feb 2026 20:31:02 -0800 Subject: [PATCH 083/105] remove field --- frontend/lib/game/timeouts.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/lib/game/timeouts.ts b/frontend/lib/game/timeouts.ts index 48e67a67..0e9e65ff 100644 --- a/frontend/lib/game/timeouts.ts +++ b/frontend/lib/game/timeouts.ts @@ -12,8 +12,6 @@ export async function getTimeout( } export default class Clock { - id: number; // FIXME: remove this field - startTimestamp: Date | null; elapsed: number; alarm: number; From 1b9d7d332408cb7abbd1a359b8277e963795da10 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Mon, 2 Feb 2026 20:13:05 -0800 Subject: [PATCH 084/105] make control buttons contextual --- frontend/app/main.tsx | 2 +- .../features/bout-control/bout-control-buttons.tsx | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 5c6ac93b..20f1e294 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -97,7 +97,7 @@ function Main() { return ( - + {bout.teams.map((team: Team, i: number) => ( diff --git a/frontend/features/bout-control/bout-control-buttons.tsx b/frontend/features/bout-control/bout-control-buttons.tsx index f25ee89d..253ab809 100644 --- a/frontend/features/bout-control/bout-control-buttons.tsx +++ b/frontend/features/bout-control/bout-control-buttons.tsx @@ -10,20 +10,23 @@ import { useRedo, useUndo } from "@/hooks/use-history"; import { timeoutQueryOptions } from "@/hooks/use-timeout"; import { Bout } from "@/lib/game/bouts"; import { Timeout } from "@/lib/game/timeouts"; +import { BoutContext } from "@/utils/contexts"; import { ActionIcon, Button, Divider, Grid, Group } from "@mantine/core"; import { IconArrowBackUp, IconArrowForwardUp } from "@tabler/icons-react"; import { useQuery } from "@tanstack/react-query"; +import { useContext } from "react"; import TimeoutButtons from "./timeout-buttons"; interface MainControlProps { bout: Bout; } -interface BoutControlButtonsProps { - bout: Bout; -} +export default function BoutControlButtons() { + const bout = useContext(BoutContext); + if (bout == null) { + throw new Error("AddTripButtons must be inside a Bout context"); + } -export default function BoutControlButtons({ bout }: BoutControlButtonsProps) { let mainControls = <>; switch (bout.state) { case "stopped": From 296cc2020e5b5972a98d6d453ce69961e05fad07 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 3 Feb 2026 19:01:14 -0800 Subject: [PATCH 085/105] update deps --- uv.lock | 109 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/uv.lock b/uv.lock index 11f7c092..fff9d1eb 100644 --- a/uv.lock +++ b/uv.lock @@ -633,7 +633,7 @@ wheels = [ [[package]] name = "pyside6" -version = "6.10.1" +version = "6.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyside6-addons" }, @@ -641,42 +641,42 @@ dependencies = [ { name = "shiboken6" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/56/22/f82cfcd1158be502c5741fe67c3fa853f3c1edbd3ac2c2250769dd9722d1/pyside6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:d0e70dd0e126d01986f357c2a555722f9462cf8a942bf2ce180baf69f468e516", size = 558169, upload-time = "2025-11-20T10:09:08.79Z" }, - { url = "https://files.pythonhosted.org/packages/66/eb/54afe242a25d1c33b04ecd8321a549d9efb7b89eef7690eed92e98ba1dc9/pyside6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4053bf51ba2c2cb20e1005edd469997976a02cec009f7c46356a0b65c137f1fa", size = 557818, upload-time = "2025-11-20T10:09:10.132Z" }, - { url = "https://files.pythonhosted.org/packages/4d/af/5706b1b33587dc2f3dfa3a5000424befba35e4f2d5889284eebbde37138b/pyside6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7d3ca20a40139ca5324a7864f1d91cdf2ff237e11bd16354a42670f2a4eeb13c", size = 558358, upload-time = "2025-11-20T10:09:11.288Z" }, - { url = "https://files.pythonhosted.org/packages/26/41/3f48d724ecc8e42cea8a8442aa9b5a86d394b85093275990038fd1020039/pyside6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9f89ff994f774420eaa38cec6422fddd5356611d8481774820befd6f3bb84c9e", size = 564424, upload-time = "2025-11-20T10:09:12.677Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/395411473b433875a82f6b5fdd0cb28f19a0e345bcaac9fbc039400d7072/pyside6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:9c5c1d94387d1a32a6fae25348097918ef413b87dfa3767c46f737c6d48ae437", size = 548866, upload-time = "2025-11-20T10:09:14.174Z" }, + { url = "https://files.pythonhosted.org/packages/35/0f/5736889fc850794623692cb369e295a994175e51295fa52134626f486296/pyside6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4b084293caa7845d0064aaf6af258e0f7caae03a14a33537d0a552131afddaf0", size = 563185, upload-time = "2026-02-02T08:50:47.161Z" }, + { url = "https://files.pythonhosted.org/packages/35/d3/ab5cd2fac3d34469c7376e0cd18eec92905dbe44748c70bda7699a2a7206/pyside6-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1b89ce8558d4b4f35b85bff1db90d680912e4d3ce9e79ff804d6fef1d1a151ef", size = 563357, upload-time = "2026-02-02T08:50:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8c/55bbd50c138c8dc12edc9f25e9d94760a33e574905468e98dff399094baa/pyside6-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:0439f5e9b10ebe6177981bac9e219096ec970ac6ec215bef055279802ba50601", size = 563357, upload-time = "2026-02-02T08:50:50.077Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d4/673b8112b4a260377f760be835c4e357163fdaf68a56a1aec59aeb8e584b/pyside6-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:032bad6b18a17fcbf4dddd0397f49b07f8aae7f1a45b7e4de7037bf7fd6e0edf", size = 569554, upload-time = "2026-02-02T08:50:51.147Z" }, + { url = "https://files.pythonhosted.org/packages/14/95/bda648fcccf61fe58cb417284716ae30acdddd44f7d4cbad6eea4ccaa872/pyside6-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:65a59ad0bc92525639e3268d590948ce07a80ee97b55e7a9200db41d493cac31", size = 553828, upload-time = "2026-02-02T08:50:52.244Z" }, ] [[package]] name = "pyside6-addons" -version = "6.10.1" +version = "6.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyside6-essentials" }, { name = "shiboken6" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/f9/b72a2578d7dbef7741bb90b5756b4ef9c99a5b40148ea53ce7f048573fe9/pyside6_addons-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4d2b82bbf9b861134845803837011e5f9ac7d33661b216805273cf0c6d0f8e82", size = 322639446, upload-time = "2025-11-20T09:54:50.75Z" }, - { url = "https://files.pythonhosted.org/packages/94/3b/3ed951c570a15570706a89d39bfd4eaaffdf16d5c2dca17e82fc3ec8aaa6/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:330c229b58d30083a7b99ed22e118eb4f4126408429816a4044ccd0438ae81b4", size = 170678293, upload-time = "2025-11-20T09:56:40.991Z" }, - { url = "https://files.pythonhosted.org/packages/22/77/4c780b204d0bf3323a75c184e349d063e208db44c993f1214aa4745d6f47/pyside6_addons-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:56864b5fecd6924187a2d0f7e98d968ed72b6cc267caa5b294cd7e88fff4e54c", size = 166365011, upload-time = "2025-11-20T09:57:20.261Z" }, - { url = "https://files.pythonhosted.org/packages/04/14/58239776499e6b279fa6ca2e0d47209531454b99f6bd2ad7c96f11109416/pyside6_addons-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:b6e249d15407dd33d6a2ffabd9dc6d7a8ab8c95d05f16a71dad4d07781c76341", size = 164864664, upload-time = "2025-11-20T09:57:54.815Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cd/1b74108671ba4b1ebb2661330665c4898b089e9c87f7ba69fe2438f3d1b6/pyside6_addons-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:0de303c0447326cdc6c8be5ab066ef581e2d0baf22560c9362d41b8304fdf2db", size = 34191225, upload-time = "2025-11-20T09:58:04.184Z" }, + { url = "https://files.pythonhosted.org/packages/61/06/c283567628ffa2cefc3c72374ad607f1dfc9842a03db65f1347b9ae52bee/pyside6_addons-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:0de7d0c9535e17d5e3b634b61314a1867f3b0f6d35c3d7cdc99efc353192faff", size = 322745605, upload-time = "2026-02-02T08:39:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/a5/69/e1ab8c756fd3984b1fd7b186446227f524f6b561160bfbfdba8874b4709a/pyside6_addons-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:030a851163b51dbf0063be59e9ddb6a9e760bde89a28e461ccc81a224d286eaf", size = 170718434, upload-time = "2026-02-02T08:40:55.989Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/18ba86ba86d1231c486d36f9accfe862ed6eb52ca0b698aeaf6e837a87ca/pyside6_addons-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:fcee0373e3fd7b98f014094e5e37b4a39e4de7c5a47c13f654a7d557d4a426ad", size = 166423836, upload-time = "2026-02-02T08:42:44.918Z" }, + { url = "https://files.pythonhosted.org/packages/99/13/503bec9201881968c372cb634069535e80aec2489f3907d676e151a1023f/pyside6_addons-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:c20150068525a17494f3b6576c5d61c417cf9a5870659e29f5ebd83cd20a78ea", size = 164712775, upload-time = "2026-02-02T08:43:23.729Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/44d6710b4dd18d745077b5fc6ded4ba6f32987a6e49c5834529e50f02155/pyside6_addons-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:3d18db739b46946ba7b722d8ad4cc2097135033aa6ea57076e64d591e6a345f3", size = 34041396, upload-time = "2026-02-02T08:43:31.246Z" }, ] [[package]] name = "pyside6-essentials" -version = "6.10.1" +version = "6.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "shiboken6" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/04/b0/c43209fecef79912e9b1c70a1b5172b1edf76caebcc885c58c60a09613b0/pyside6_essentials-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:cd224aff3bb26ff1fca32c050e1c4d0bd9f951a96219d40d5f3d0128485b0bbe", size = 105461499, upload-time = "2025-11-20T09:59:23.733Z" }, - { url = "https://files.pythonhosted.org/packages/5f/8e/b69ba7fa0c701f3f4136b50460441697ec49ee6ea35c229eb2a5ee4b5952/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e9ccbfb58c03911a0bce1f2198605b02d4b5ca6276bfc0cbcf7c6f6393ffb856", size = 76764617, upload-time = "2025-11-20T09:59:38.831Z" }, - { url = "https://files.pythonhosted.org/packages/bd/83/569d27f4b6c6b9377150fe1a3745d64d02614021bea233636bc936a23423/pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ec8617c9b143b0c19ba1cc5a7e98c538e4143795480cb152aee47802c18dc5d2", size = 75850373, upload-time = "2025-11-20T09:59:56.082Z" }, - { url = "https://files.pythonhosted.org/packages/1e/64/a8df6333de8ccbf3a320e1346ca30d0f314840aff5e3db9b4b66bf38e26c/pyside6_essentials-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9555a48e8f0acf63fc6a23c250808db841b28a66ed6ad89ee0e4df7628752674", size = 74491180, upload-time = "2025-11-20T10:00:11.215Z" }, - { url = "https://files.pythonhosted.org/packages/67/da/65cc6c6a870d4ea908c59b2f0f9e2cf3bfc6c0710ebf278ed72f69865e4e/pyside6_essentials-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:4d1d248644f1778f8ddae5da714ca0f5a150a5e6f602af2765a7d21b876da05c", size = 55190458, upload-time = "2025-11-20T10:00:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2e/5f18a77f5e0bd730bacec93a690d0ef3c96a9711d213653eacecbf241b8d/pyside6_essentials-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:1dee2cb9803ff135f881dadeb5c0edcef793d1ec4f8a9140a1348cecb71074e1", size = 105913067, upload-time = "2026-02-02T08:45:37.508Z" }, + { url = "https://files.pythonhosted.org/packages/99/20/3a6ca95052e1744b5a3eba164e2dd451d358a3dcaf78179de4b45c8e3f47/pyside6_essentials-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:660aea45bfa36f1e06f799b934c2a7df963bd31abc5083e8bb8a5bfaef45686b", size = 77027153, upload-time = "2026-02-02T08:45:53.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/a6/6073e4ddc2a5c7b3941606e4bc8bbaadcf0737f57450620b0793041c8d22/pyside6_essentials-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c2b028e4c6f8047a02c31f373408e23b4eedfd405f56c6aba8d0525c29472835", size = 76114242, upload-time = "2026-02-02T08:46:07.184Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/616bbbd009efd3e17bf9a2db09d90c6764c010565cd2bdea2a240bfd18f7/pyside6_essentials-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:0741018c2b6395038cad4c41775cfae3f13a409e87995ac9f7d89e5b1fb6b22a", size = 74546490, upload-time = "2026-02-02T08:46:26.395Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f9/c9757a984c4ffb6d12fab69e966d95dfc862a5d44e12b7900f3a03780b76/pyside6_essentials-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:db5f4913648bb6afddb8b347edae151ee2378f12bceb03c8b2515a530a4b38d9", size = 55258626, upload-time = "2026-02-02T08:46:36.788Z" }, ] [[package]] @@ -757,29 +757,29 @@ wheels = [ [[package]] name = "rich" -version = "14.3.1" +version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] name = "rich-toolkit" -version = "0.17.1" +version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/09/3f9b8d9daaf235195c626f21e03604c05b987404ee3bcacee0c1f67f2a8e/rich_toolkit-0.17.1.tar.gz", hash = "sha256:5af54df8d1dd9c8530e462e1bdcaed625c9b49f5a55b035aa0ba1c17bdb87c9a", size = 187925, upload-time = "2025-12-17T10:49:22.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/f1/bcfbde3ca38db54b5dcf7ee3d0caf3ed9133a169aec5a58ad9ec50ba12e8/rich_toolkit-0.18.1.tar.gz", hash = "sha256:bf104f1945a7252debeda7d7138118eaf848fff5ea81d9eda556cbc5f911122c", size = 192514, upload-time = "2026-02-01T10:56:31.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/7b/15e55fa8a76d0d41bf34d965af78acdaf80a315907adb30de8b63c272694/rich_toolkit-0.17.1-py3-none-any.whl", hash = "sha256:96d24bb921ecd225ffce7c526a9149e74006410c05e6d405bd74ffd54d5631ed", size = 31412, upload-time = "2025-12-17T10:49:21.793Z" }, + { url = "https://files.pythonhosted.org/packages/da/43/6f9860c4bfb1f181c347941542a8955ce24b228f84550253765aa1854d53/rich_toolkit-0.18.1-py3-none-any.whl", hash = "sha256:04011a9751f4c2becdf44bd1aaff8562d4b00caf04f14e483a9873c15fbe3154", size = 32255, upload-time = "2026-02-01T10:56:33.071Z" }, ] [[package]] @@ -807,28 +807,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, - { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, - { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, - { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +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]] @@ -842,15 +841,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.50.0" +version = "2.51.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/8a/3c4f53d32c21012e9870913544e56bfa9e931aede080779a0f177513f534/sentry_sdk-2.50.0.tar.gz", hash = "sha256:873437a989ee1b8b25579847bae8384515bf18cfed231b06c591b735c1781fe3", size = 401233, upload-time = "2026-01-20T12:53:16.244Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/9f/094bbb6be5cf218ab6712c6528310687f3d3fe8818249fcfe1d74192f7c5/sentry_sdk-2.51.0.tar.gz", hash = "sha256:b89d64577075fd8c13088bc3609a2ce77a154e5beb8cba7cc16560b0539df4f7", size = 407447, upload-time = "2026-01-28T10:29:50.962Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/5b/cbc2bb9569f03c8e15d928357e7e6179e5cfab45544a3bbac8aec4caf9be/sentry_sdk-2.50.0-py2.py3-none-any.whl", hash = "sha256:0ef0ed7168657ceb5a0be081f4102d92042a125462d1d1a29277992e344e749e", size = 424961, upload-time = "2026-01-20T12:53:14.826Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/df379404d484ca9dede4ad8abead5de828cdcff35623cd44f0351cf6869c/sentry_sdk-2.51.0-py2.py3-none-any.whl", hash = "sha256:e21016d318a097c2b617bb980afd9fc737e1efc55f9b4f0cdc819982c9717d5f", size = 431426, upload-time = "2026-01-28T10:29:48.868Z" }, ] [[package]] @@ -873,14 +872,14 @@ wheels = [ [[package]] name = "shiboken6" -version = "6.10.1" +version = "6.10.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/8b/e5db743d505ceea3efc4cd9634a3bee22a3e2bf6e07cefd28c9b9edabcc6/shiboken6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:9f2990f5b61b0b68ecadcd896ab4441f2cb097eef7797ecc40584107d9850d71", size = 478483, upload-time = "2025-11-20T10:08:52.411Z" }, - { url = "https://files.pythonhosted.org/packages/56/ba/b50c1a44b3c4643f482afbf1a0ea58f393827307100389ce29404f9ad3b0/shiboken6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4221a52dfb81f24a0d20cc4f8981cb6edd810d5a9fb28287ce10d342573a0e4", size = 271993, upload-time = "2025-11-20T10:08:54.093Z" }, - { url = "https://files.pythonhosted.org/packages/16/b8/939c24ebd662b0aa5c945443d0973145b3fb7079f0196274ef7bb4b98f73/shiboken6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c095b00f4d6bf578c0b2464bb4e264b351a99345374478570f69e2e679a2a1d0", size = 268691, upload-time = "2025-11-20T10:08:55.639Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a6/8c65ee0fa5e172ebcca03246b1bc3bd96cdaf1d60537316648536b7072a5/shiboken6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:c1601d3cda1fa32779b141663873741b54e797cb0328458d7466281f117b0a4e", size = 1234704, upload-time = "2025-11-20T10:08:57.417Z" }, - { url = "https://files.pythonhosted.org/packages/7b/6a/c0fea2f2ac7d9d96618c98156500683a4d1f93fea0e8c5a2bc39913d7ef1/shiboken6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:5cf800917008587b551005a45add2d485cca66f5f7ecd5b320e9954e40448cc9", size = 1795567, upload-time = "2025-11-20T10:08:59.184Z" }, + { url = "https://files.pythonhosted.org/packages/fb/38/3912eb08a3b865b5fcdb4bdce8076cacc211986cee587f5cb62e637791af/shiboken6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:3bd4e94e9a3c8c1fa8362fd752d399ef39265d5264e4e37bae61cdaa2a00c8c7", size = 479829, upload-time = "2026-02-02T08:50:22.495Z" }, + { url = "https://files.pythonhosted.org/packages/52/88/292e0576489c46624ab419ee284ac5a59ae10e2eb34a58b6abca51dfd290/shiboken6-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ace0790032d9cb0adda644b94ee28d59410180d9773643bb6cf8438c361987ad", size = 273052, upload-time = "2026-02-02T08:50:24.539Z" }, + { url = "https://files.pythonhosted.org/packages/06/c2/03d44d34e8264e1f25671677fece95b414c70fd85dcc2be8d5e821ee2628/shiboken6-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:f74d3ed1f92658077d0630c39e694eb043aeb1d830a5d275176c45d07147427f", size = 269868, upload-time = "2026-02-02T08:50:25.662Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/5ca52c0ef86b3d01572131b6709bd531a080995f7e680720e9424328ce1d/shiboken6-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:10f3c8c5e1b8bee779346f21c10dbc14cff068f0b0b4e62420c82a6bf36ac2e7", size = 1222052, upload-time = "2026-02-02T08:50:27.502Z" }, + { url = "https://files.pythonhosted.org/packages/46/52/421fd378313c89b67ee7d584bf4e9ec088fa1804891b8d74e02b16703457/shiboken6-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:20c671645d70835af212ee05df60361d734c5305edb2746e9875c6a31283f963", size = 1784089, upload-time = "2026-02-02T08:50:29.069Z" }, ] [[package]] From 7dc39b3ea0b4f687bbcf5a53be5d462eed7d310f Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 3 Feb 2026 19:23:56 -0800 Subject: [PATCH 086/105] fix blink behavior --- frontend/components/timeout-bar.tsx | 12 ++++++++---- frontend/features/team-view/team-view.tsx | 12 ++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/components/timeout-bar.tsx b/frontend/components/timeout-bar.tsx index a6dd3653..b88f16b0 100644 --- a/frontend/components/timeout-bar.tsx +++ b/frontend/components/timeout-bar.tsx @@ -3,11 +3,12 @@ import { IconCircleFilled } from "@tabler/icons-react"; import { twMerge } from "tailwind-merge"; interface TimeoutBarProps { - activeType?: "timeout" | "review"; numTimeouts: number; timeoutsRemaining: number; numReviews: number; reviewsRemaining: number; + timeoutIsActive: boolean; + isReview: boolean; size: number; } @@ -37,9 +38,10 @@ function TimeoutPip({ export default function TimeoutBar({ numTimeouts, numReviews, - activeType, timeoutsRemaining, reviewsRemaining, + timeoutIsActive, + isReview, size = 30, }: TimeoutBarProps) { return ( @@ -50,7 +52,9 @@ export default function TimeoutBar({ = timeoutsRemaining} - active={i == timeoutsRemaining - 1 && activeType == "timeout"} + active={ + i == timeoutsRemaining - 1 && timeoutIsActive && !isReview + } />
@@ -64,7 +68,7 @@ export default function TimeoutBar({ = reviewsRemaining} - active={i == reviewsRemaining - 1 && activeType == "review"} + active={i == reviewsRemaining - 1 && timeoutIsActive && isReview} />
diff --git a/frontend/features/team-view/team-view.tsx b/frontend/features/team-view/team-view.tsx index 3c99e3f8..1047466b 100644 --- a/frontend/features/team-view/team-view.tsx +++ b/frontend/features/team-view/team-view.tsx @@ -25,13 +25,6 @@ export default function TeamView({ team }: TeamsViewProps) { placeholderData: undefined, }); - const timeoutType: "review" | "timeout" | undefined = - timeout == undefined || timeout.teamIsOfficials - ? undefined - : timeout.isReview - ? "review" - : "timeout"; - return (
@@ -41,11 +34,14 @@ export default function TeamView({ team }: TeamsViewProps) {
From bdb525c565b383f28b3f302f0e44deb78dc9c290 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 3 Feb 2026 20:10:49 -0800 Subject: [PATCH 087/105] make team_num optional --- backend/src/game/timeouts/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/game/timeouts/router.py b/backend/src/game/timeouts/router.py index 89b11c82..aa7f7831 100644 --- a/backend/src/game/timeouts/router.py +++ b/backend/src/game/timeouts/router.py @@ -29,7 +29,7 @@ async def set_type( @router.post('/team', tags=[TIMEOUTS_TAG]) async def set_team( - timeout: GetTimeout, team_num: Annotated[int | None, Body()] + timeout: GetTimeout, team_num: Annotated[int | None, Body()] = None ) -> None: """Set the calling Team of the specified Timeout.""" bout: BaseBout = await timeout.get_bout() From d892f58e265abe0090f11e4a8ab010c45823eb54 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 3 Feb 2026 20:11:19 -0800 Subject: [PATCH 088/105] catch correct error --- backend/src/game/timeouts/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/game/timeouts/dependencies.py b/backend/src/game/timeouts/dependencies.py index c6034c76..03113b83 100644 --- a/backend/src/game/timeouts/dependencies.py +++ b/backend/src/game/timeouts/dependencies.py @@ -18,7 +18,7 @@ async def _get_timeout( ) -> BaseTimeout: try: timeout: BaseTimeout = bout.timeouts[num] - except KeyError as e: + except IndexError as e: raise ModelLookupError(f'Could not find Timeout ({bout=} {num=})') from e # Optionally take a snapshot of the Timeout state From 85aa58a9000613ee73b0252e3a6d01fd69f68058 Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 3 Feb 2026 20:14:57 -0800 Subject: [PATCH 089/105] fix api call --- frontend/lib/game/timeouts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/lib/game/timeouts.ts b/frontend/lib/game/timeouts.ts index 0e9e65ff..98736460 100644 --- a/frontend/lib/game/timeouts.ts +++ b/frontend/lib/game/timeouts.ts @@ -60,7 +60,7 @@ export class Timeout { async setTeam(teamNum: number | null): Promise { await localAPI.post("timeout/team", { query: { boutUuid: this.boutUuid, num: this.num }, - body: JSON.stringify(teamNum), // FIXME: this doesn't work when null + body: teamNum, }); } From e59c4e7a5fe0bd0ec4a373d75ef2024f301a903d Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 3 Feb 2026 20:18:04 -0800 Subject: [PATCH 090/105] timeouts can be orphaned by team --- backend/src/game/teams/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/game/teams/models.py b/backend/src/game/teams/models.py index 94e87e44..6648492e 100644 --- a/backend/src/game/teams/models.py +++ b/backend/src/game/teams/models.py @@ -65,7 +65,7 @@ class BaseTeam(BaseSQLModel): ) timeouts: Mapped[list[BaseTimeout]] = relationship( back_populates='team', - cascade=CASCADE_CHILD, + cascade='all', # Exclude `delete-orphan` as Timeouts can be called by officials lazy='selectin', order_by=[BaseTimeout.num], ) From 7a6520d45e302624831bce1295a627784d8ae3dd Mon Sep 17 00:00:00 2001 From: Mitch Weisbrod Date: Tue, 3 Feb 2026 23:32:48 -0800 Subject: [PATCH 091/105] add bout picker component --- frontend/app/main.tsx | 27 ++++++++++++----------- frontend/components/bout-picker.tsx | 34 +++++++++++++++++++++++++++++ frontend/hooks/use-bout.ts | 12 ++++++---- 3 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 frontend/components/bout-picker.tsx diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 20f1e294..f6826c65 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -1,14 +1,13 @@ +import BoutPicker from "@/components/bout-picker"; import BoutControlButtons from "@/features/bout-control/bout-control-buttons"; import BoutStateView from "@/features/bout-state-view/bout-state-view"; import TeamJamView from "@/features/team-jam-vew/team-jam-view"; import TeamView from "@/features/team-view/team-view"; -import { useSuspenseBout } from "@/hooks/use-bout"; +import { useSuspenseAllBouts, useSuspenseBout } from "@/hooks/use-bout"; import { useJam, useSuspenseJam } from "@/hooks/use-jam"; -import { useSuspenseAllSeries } from "@/hooks/use-series"; import { usePrefetchServerTime } from "@/hooks/use-server-time"; import queryClient from "@/lib/cache"; import { Team } from "@/lib/game/bouts"; -import { Series } from "@/lib/game/series"; import { redo, undo } from "@/lib/history"; import { BoutContext } from "@/utils/contexts"; import { @@ -21,7 +20,7 @@ import { import "@mantine/core/styles.css"; import { useDisclosure } from "@mantine/hooks"; import { QueryClientProvider } from "@tanstack/react-query"; -import { StrictMode, Suspense } from "react"; +import { StrictMode, Suspense, useState } from "react"; import { createRoot } from "react-dom/client"; import "./global.css"; @@ -41,6 +40,10 @@ createRoot(root).render(); export default function App() { const [opened, { toggle }] = useDisclosure(); + + const { data: bouts } = useSuspenseAllBouts(); + const [boutUuid, setBoutUuid] = useState(bouts[bouts.length - 1].uuid); + return ( @@ -63,11 +66,14 @@ export default function App() { /> - {/* TODO: Navbar */} + + setBoutUuid(uuid)} /> + {/* TODO: Navbar */} + -
+
@@ -77,15 +83,10 @@ export default function App() { ); } -function Main() { +function Main({ boutUuid }: { boutUuid: string }) { usePrefetchServerTime(); - const { data: allSeries } = useSuspenseAllSeries(); - - const series: Series = allSeries[0]; - const { data: bout } = useSuspenseBout( - series.boutUuids[series.activeBoutIndex ?? series.boutUuids.length - 1], - ); + const { data: bout } = useSuspenseBout(boutUuid); // Eagerly query the latest Jam and Timeout to avoid suspending void useJam(bout, ...bout.getLatestJamNum()); diff --git a/frontend/components/bout-picker.tsx b/frontend/components/bout-picker.tsx new file mode 100644 index 00000000..562ab5b3 --- /dev/null +++ b/frontend/components/bout-picker.tsx @@ -0,0 +1,34 @@ +import { useSuspenseAllBouts } from "@/hooks/use-bout"; +import { Bout } from "@/lib/game/bouts"; +import { Select } from "@mantine/core"; +import { useEffect, useState } from "react"; + +interface BoutPickerProps { + onChange: (boutUUid: string) => void; +} + +export default function BoutPicker({ onChange }: BoutPickerProps) { + const { data: bouts } = useSuspenseAllBouts(); + + const selectableData = bouts.map((bout: Bout) => ({ + value: bout.uuid, + label: `${bout.teams[0].name} vs. ${bout.teams[1].name}`, + })); + + const [selectedBoutUuid, setSelectedBoutUuid] = useState( + selectableData[selectableData.length - 1].value, + ); + + useEffect(() => { + onChange(selectedBoutUuid); + }, [selectedBoutUuid, onChange]); + + return ( +