Compare commits

..
Author SHA1 Message Date
renovate-bot 6b1e252dc9 Update python Docker tag to v3.14.8 2026-10-04 00:00:39 +00:00
imcompiling 8016b034ac Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.9.18' (#110) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 1m7s
Reviewed-on: #110
Reviewed-by: imcompiling <[email protected]>
2025-12-21 13:25:35 -05:00
renovate-bot 5f4fcc28ae Update ghcr.io/astral-sh/uv Docker tag to v0.9.18 2025-12-21 00:00:43 +00:00
imcompiling a99a76a3e0 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.9.17' (#108) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 1m10s
Reviewed-on: #108
Reviewed-by: imcompiling <[email protected]>
2025-12-16 21:38:06 -05:00
renovate-bot b7028c112d Update ghcr.io/astral-sh/uv Docker tag to v0.9.17 2025-12-14 00:00:41 +00:00
imcompiling 2229a383c5 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.8.11' (#106) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 1m4s
Reviewed-on: #106
Reviewed-by: imcompiling <[email protected]>
2025-08-20 10:27:41 -04:00
renovate-bot ea1f1a5b3d Update ghcr.io/astral-sh/uv Docker tag to v0.8.11 2025-08-17 04:00:35 +00:00
imcompiling db2657a5b3 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.8.3' (#105) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 59s
Reviewed-on: #105
Reviewed-by: imcompiling <[email protected]>
2025-07-28 09:03:36 -04:00
renovate-bot ff2c233381 Update ghcr.io/astral-sh/uv Docker tag to v0.8.3 2025-07-27 04:00:33 +00:00
imcompiling ab9176b10d Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.8.0' (#104) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 1m8s
Reviewed-on: #104
Reviewed-by: imcompiling <[email protected]>
2025-07-21 09:15:11 -04:00
renovate-bot 7c77603711 Update ghcr.io/astral-sh/uv Docker tag to v0.8.0 2025-07-20 04:00:40 +00:00
imcompiling 5cc41b8e8c Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.7.20' (#103) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 59s
Reviewed-on: #103
Reviewed-by: imcompiling <[email protected]>
2025-07-15 09:03:32 -04:00
renovate-bot d470ca3a8f Update ghcr.io/astral-sh/uv Docker tag to v0.7.20 2025-07-13 04:00:39 +00:00
imcompiling f2d55f435d Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.7.12' (#102) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 57s
Reviewed-on: #102
Reviewed-by: imcompiling <[email protected]>
2025-06-09 09:09:31 -04:00
renovate-bot ea63c5ea88 Update ghcr.io/astral-sh/uv Docker tag to v0.7.12 2025-06-08 04:01:08 +00:00
imcompiling b01616f744 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.7.9' (#101) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 57s
Reviewed-on: #101
Reviewed-by: imcompiling <[email protected]>
2025-06-02 09:28:57 -04:00
renovate-bot 811ec5c7ee Update ghcr.io/astral-sh/uv Docker tag to v0.7.9 2025-06-01 04:01:08 +00:00
imcompiling 1950bcdc16 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.7.8' (#100) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 1m5s
Reviewed-on: #100
Reviewed-by: imcompiling <[email protected]>
2025-05-28 08:29:50 -04:00
renovate-bot 972ecef2be Update ghcr.io/astral-sh/uv Docker tag to v0.7.8 2025-05-25 04:01:09 +00:00
imcompiling 540efc8819 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.7.5' (#99) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 1m2s
Reviewed-on: #99
Reviewed-by: imcompiling <[email protected]>
2025-05-21 11:20:30 -04:00
renovate-bot 7d02608a14 Update ghcr.io/astral-sh/uv Docker tag to v0.7.5 2025-05-18 04:01:11 +00:00
imcompiling 7861e498c5 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.7.3' (#98) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 57s
Reviewed-on: #98
Reviewed-by: imcompiling <[email protected]>
2025-05-12 09:53:13 -04:00
renovate-bot dd28de3cac Update ghcr.io/astral-sh/uv Docker tag to v0.7.3 2025-05-11 04:01:21 +00:00
imcompiling cf872c071b Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.6.17' (#97) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 56s
Reviewed-on: #97
Reviewed-by: imcompiling <[email protected]>
2025-04-28 12:13:06 -04:00
renovate-bot 6c6b5cac25 Update ghcr.io/astral-sh/uv Docker tag to v0.6.17 2025-04-27 04:01:07 +00:00
imcompiling 50ce8fbfa3 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.6.14' (#95) from renovate/ghcr.io-astral-sh-uv-0.x into main
Reviewed-on: #95
2025-04-13 10:32:24 -04:00
renovate-bot d0ef8e723f Update ghcr.io/astral-sh/uv Docker tag to v0.6.14 2025-04-13 14:13:11 +00:00
imcompiling c434154d84 Merge branch '90-00-in-score'
build_latest / build (push) Successful in 32s
build_release / build (push) Successful in 17s
2025-04-11 09:10:28 -04:00
imcompiling bc038c8d74 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.6.12' (#94) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 57s
Reviewed-on: #94
Reviewed-by: imcompiling <[email protected]>
2025-04-06 11:58:45 -04:00
renovate-bot 7528552280 Update ghcr.io/astral-sh/uv Docker tag to v0.6.12 2025-04-06 04:01:34 +00:00
imcompiling b71669630a Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.6.10' (#93) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Successful in 54s
Reviewed-on: #93
Reviewed-by: imcompiling <[email protected]>
2025-03-30 12:47:06 -04:00
renovate-bot 52eb92d527 Update ghcr.io/astral-sh/uv Docker tag to v0.6.10 2025-03-30 04:01:32 +00:00
imcompiling 46bce400af Merge pull request 'Update actions/checkout action to v4' (#92) from renovate/actions-checkout-4.x into main
build_latest / build (push) Successful in 47s
build_release / build (push) Successful in 19s
Reviewed-on: #92
Reviewed-by: imcompiling <[email protected]>
2025-03-23 23:38:13 -04:00
imcompiling b681d7a923 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.6.9' (#91) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_latest / build (push) Has been cancelled
Reviewed-on: #91
Reviewed-by: imcompiling <[email protected]>
2025-03-23 23:37:49 -04:00
renovate-bot ec086448ea Update actions/checkout action to v4 2025-03-23 04:01:09 +00:00
renovate-bot 476b0e89ef Update ghcr.io/astral-sh/uv Docker tag to v0.6.9 2025-03-23 04:01:04 +00:00
imcompiling c1b327ca05 Change regex to strip a 10th place 0 only.
build_develop / build (push) Successful in 36s
2025-03-21 13:56:28 -04:00
imcompiling fa39949b1a Merge branch 'main' of https://gitea.cheezers.org/imcompiling/agon
build_latest / build (push) Successful in 37s
2025-03-21 10:45:04 -04:00
imcompiling f1290af45b Remove a double respond. 2025-03-21 10:44:57 -04:00
imcompiling 10954ef117 Merge branch '82-group-message-after-deletion'
build_latest / build (push) Successful in 36s
build_release / build (push) Successful in 18s
2025-03-20 11:04:26 -04:00
imcompiling 0540f7a9eb Add thread as an option everywhere a textchannel is used.
build_develop / build (push) Successful in 35s
2025-03-20 11:04:00 -04:00
imcompiling a4aab09360 Use or instead of two isinstance calls. 2025-03-20 10:56:59 -04:00
imcompiling af7518c1a2 Delete old group message when changing output channel.
build_develop / build (push) Successful in 29s
2025-03-20 10:55:48 -04:00
imcompiling 5be029fb57 Catch when the message has been deleted without entry deletion in my database. 2025-03-20 10:42:24 -04:00
imcompiling d82999b141 Check for channel being None. Allow groups to be posted in threads. 2025-03-20 09:50:31 -04:00
imcompiling 1ffb7fa9a7 Correct database ignore.
build_develop / build (push) Successful in 18s
build_latest / build (push) Successful in 18s
build_release / build (push) Successful in 19s
2025-03-19 09:51:38 -04:00
imcompiling 2566bee0ce Use home dir.
build_develop / build (push) Successful in 36s
2025-03-19 09:37:32 -04:00
imcompiling 3d113a06d4 Update workflows.
build_develop / build (push) Successful in 35s
2025-03-19 09:35:21 -04:00
imcompiling 2e3b8c86da Add more to dockerignore. 2025-03-19 09:30:44 -04:00
imcompiling 058e3aefa1 Update how agon is run. 2025-03-19 09:28:30 -04:00
imcompiling 7a4ba3a5af Update all imports with new file structure. 2025-03-19 09:22:29 -04:00
imcompiling 2f5f25ea75 Remove unneede file. 2025-03-19 09:17:43 -04:00
imcompiling fd476ce512 Move files from utilities to top level of module. 2025-03-19 09:17:07 -04:00
imcompiling 9588cdc8a0 Rename main.py to app.py, add init and main dunder files. 2025-03-19 09:11:02 -04:00
imcompiling 17769022bc Move all files into agon module. 2025-03-19 09:07:39 -04:00
imcompiling a46dc0388c Delete sql file. it is unneeded now. 2025-03-19 09:07:14 -04:00
imcompiling 553f79425f Delete database/pyproject.toml
build_develop / build (push) Successful in 36s
2025-03-19 08:49:17 -04:00
imcompiling 72cd6e9fd1 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.6.6' (#87) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_develop / build (push) Successful in 56s
Reviewed-on: #87
Reviewed-by: imcompiling <[email protected]>
2025-03-16 11:09:44 -04:00
renovate-bot f63984e25a Update ghcr.io/astral-sh/uv Docker tag to v0.6.6 2025-03-16 04:01:28 +00:00
imcompiling 48a91dca3d Merge branch '41-editting-commands'
build_develop / build (push) Successful in 35s
build_release / build (push) Successful in 22s
2025-03-14 17:22:03 -04:00
imcompiling 5eb637a3a8 Indent so that a command can properly use session.
Remove commented out code. Also add work to an issue.
2025-03-14 17:20:50 -04:00
imcompiling f722eb1a83 Add to get's return messages. 2025-03-14 17:05:39 -04:00
imcompiling 7759bbc648 Add to set's return messages. 2025-03-14 17:04:20 -04:00
imcompiling 65de7baeda Added some log statements to provide helpful information. 2025-03-14 16:57:38 -04:00
imcompiling e243404480 Forgot to specify that is a description. 2025-03-14 16:53:15 -04:00
imcompiling f735e6d846 Merge branch '70-sqlmodel'
build_develop / build (push) Successful in 34s
build_release / build (push) Successful in 22s
2025-03-14 16:43:28 -04:00
imcompiling 9fbaf7bcf6 Merge branch 'main' of https://gitea.cheezers.org/imcompiling/agon
build_develop / build (push) Has been cancelled
2025-03-14 16:42:48 -04:00
imcompiling 1959e11e5c Merge branch '70-sqlmodel' into 41-editting-commands 2025-03-14 16:42:33 -04:00
imcompiling c25d354fc0 Correct some parameter usage, handle errors if searches return nothing. 2025-03-14 16:09:52 -04:00
imcompiling e5d90a71d9 Correct parameter usage. 2025-03-14 16:02:18 -04:00
imcompiling 785a4eff46 Was saving the message id to the wrong spot. 2025-03-13 09:21:10 -04:00
imcompiling 627112b114 Use correct message id reference. 2025-03-13 09:21:09 -04:00
imcompiling 9b3bd9d02c Use correct variable for guild name. 2025-03-13 09:21:09 -04:00
imcompiling 2c56c614f3 Typo on col usage. 2025-03-13 09:21:09 -04:00
imcompiling 3ccaa7004e Had col around the wrong variables. 2025-03-13 09:21:08 -04:00
imcompiling 289113661f Use walrus operator when assigning and checking for None. 2025-03-13 09:21:08 -04:00
imcompiling 74ce22dd26 Was saving the message id to the wrong spot. 2025-03-13 00:39:47 -04:00
imcompiling 170ce1cbfb Use correct message id reference. 2025-03-12 21:30:56 -04:00
imcompiling e1b761e848 Use correct variable for guild name. 2025-03-12 21:29:01 -04:00
imcompiling 68592a7df8 Typo on col usage. 2025-03-12 21:27:29 -04:00
imcompiling b28938f66a Had col around the wrong variables. 2025-03-12 21:27:12 -04:00
imcompiling adab97f505 Add another TODO 2025-03-12 10:18:16 -04:00
imcompiling edf51aac2d Add a todo regarding possible bug. 2025-03-12 10:12:54 -04:00
imcompiling dbc6474fc3 send highscore message doesn't need to return the message, its never used. 2025-03-12 10:02:32 -04:00
imcompiling ed68c4174f Update send highscore message to be more robust. 2025-03-12 09:57:17 -04:00
imcompiling 3abbff13d5 Use walrus operator when assigning and checking for None. 2025-03-12 09:50:58 -04:00
imcompiling 304b089113 Use the walrus operator when assigning and checking for None. 2025-03-12 09:35:44 -04:00
imcompiling 29a163ec46 correct some sql statement usages. Wasn't matching things properly. 2025-03-11 09:37:27 -04:00
imcompiling 76fbba739f Add command to view a group's username length. 2025-03-11 09:12:54 -04:00
imcompiling 7204e882f1 Forgot to do group autocomplete. 2025-03-11 09:10:17 -04:00
imcompiling a8d775ca09 Correct some doc strings. 2025-03-11 09:05:36 -04:00
imcompiling 983b9b6f34 Add command for getting group's pad length. 2025-03-11 09:03:54 -04:00
imcompiling 84c910b19c Add get is_validated command. 2025-03-10 16:06:28 -04:00
imcompiling 1fcb25193d Add get is_ascending command and autocomplete functions. 2025-03-10 14:27:24 -04:00
imcompiling f52b2fe49f Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.6.5' (#81) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_develop / build (push) Successful in 48s
Reviewed-on: #81
Reviewed-by: imcompiling <[email protected]>
2025-03-10 08:40:41 -04:00
renovate-bot 24a9fa998a Update ghcr.io/astral-sh/uv Docker tag to v0.6.5 2025-03-09 05:01:04 +00:00
imcompiling 4785271ebb Add command to get a category's size. 2025-03-07 16:10:42 -05:00
imcompiling f9d765ad92 Forgot to convert some ids to int. 2025-03-07 16:09:33 -05:00
imcompiling 50868988c0 Create teh get cog, add it to init for commands. 2025-03-07 15:59:13 -05:00
imcompiling 81ae867a43 Add set group and category name commands. 2025-03-07 12:56:45 -05:00
imcompiling 2649ceb6bb Add command to edit is_validated. 2025-03-07 11:45:05 -05:00
imcompiling 6e02bc877f Only want a warning as I can get the group another way. 2025-03-07 11:40:27 -05:00
imcompiling f61ad83e11 Make commands more robust against errors. 2025-03-07 11:39:39 -05:00
imcompiling feeb47d3da Update the message when updating pad length. 2025-03-07 11:24:45 -05:00
imcompiling 0ff1b07096 Add category and group subcommand structure. 2025-03-05 15:47:01 -05:00
imcompiling 8674eed0c5 Forgot a command description. 2025-03-05 15:32:20 -05:00
imcompiling e2ddd5412a Add set is_ascending command. 2025-03-05 15:27:57 -05:00
imcompiling f37844c953 Update error message. 2025-03-05 11:05:07 -05:00
imcompiling d703c76704 Add a command to set the category size. 2025-03-05 11:02:09 -05:00
imcompiling 6e25d4e75a Move the logger into own module for importing. 2025-03-05 10:50:37 -05:00
imcompiling 0702198f0a Fix lint smells and update model usage in view. 2025-03-04 13:24:08 -05:00
imcompiling 43df7aeda7 Fix lint smells and update model usage in update. 2025-03-04 13:20:14 -05:00
imcompiling c8becb8203 Fix lint smells and update model usage in submission. 2025-03-04 13:15:20 -05:00
imcompiling 44d813504a Fix lint smells and update model usage in refresh and set. 2025-03-04 12:47:44 -05:00
imcompiling 3fc8910c9a Fix lint smell. 2025-03-04 11:19:06 -05:00
imcompiling 563b6ab888 Fix lint smells. Update model usage. 2025-03-04 11:18:40 -05:00
imcompiling d7e44d3a22 Fix lint smells and update model usage. 2025-03-04 11:04:10 -05:00
imcompiling a99b488ae1 Message id isn't known at object creation time. 2025-03-04 11:03:45 -05:00
imcompiling 85d0077107 Fix smells and update model uses for updating username verify. 2025-03-04 09:53:09 -05:00
imcompiling 9510c9eddc Add init for views directory. Fix smells and update model usage in delete confirm. 2025-03-04 09:47:39 -05:00
imcompiling 9550d1a68b Remove all pylint supressors. 2025-03-03 23:41:58 -05:00
imcompiling b04819aa36 Fix smells and model usage in guild listener. 2025-03-03 23:40:49 -05:00
imcompiling f43dccb12d Finish fixing lint smells in loops directory. 2025-03-03 23:36:07 -05:00
imcompiling 9cf05f44d0 Fix error in model.
The two channel ids need to be NULL by default, don't know them on object creation.
2025-03-03 23:32:03 -05:00
imcompiling f6ae34c9e6 Started fixing smells and updating model calls. 2025-03-03 23:29:46 -05:00
imcompiling 528af14e00 Update model usage in verification view. 2025-03-03 23:14:31 -05:00
imcompiling dbe652dcce Fix smells in submission init file. 2025-03-03 23:08:53 -05:00
imcompiling 1658638938 Remove unneeded parenthesis. 2025-03-03 23:08:41 -05:00
imcompiling 63db4c1dd2 Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.6.3' (#79) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_develop / build (push) Successful in 51s
Reviewed-on: #79
Reviewed-by: imcompiling <[email protected]>
2025-03-03 23:03:42 -05:00
imcompiling 43f51f7f07 Test file after connecting to prod database and testing relationships. 2025-03-03 23:02:40 -05:00
imcompiling f74b08b02e Add another word to cspell. 2025-03-03 23:02:19 -05:00
imcompiling 9dd07bef93 Add an init file for utilities. 2025-03-03 23:01:58 -05:00
imcompiling 231c700946 Fix other errors with models. 2025-03-03 22:57:48 -05:00
imcompiling 37daa0e657 Update names used in constraints. 2025-03-03 22:46:33 -05:00
imcompiling 6dfd3acad0 Add SnakeCase column names that are already in database. 2025-03-03 22:44:14 -05:00
imcompiling ce3ed3c001 Just more testing. 2025-03-03 21:53:19 -05:00
imcompiling 0ad425b982 Small lint adjustments. 2025-03-03 21:52:54 -05:00
imcompiling cf5629a6dd Finish updating utils to use new models. 2025-03-03 21:52:30 -05:00
imcompiling 37fb9c1abb Use col to correct type hint errors. 2025-03-03 21:01:59 -05:00
imcompiling 157b8d4f3f Begin updating utils. 2025-03-03 15:54:28 -05:00
imcompiling 1c3fb231b7 Add more cspell words. 2025-03-03 15:54:20 -05:00
imcompiling b72dd35e04 Add init file to the database package. 2025-03-03 15:28:00 -05:00
imcompiling 0b2a1cd293 Organize import in db.py. 2025-03-03 15:19:46 -05:00
imcompiling f1f0c0c6de Rename database_setup directory to database. 2025-03-03 15:18:31 -05:00
imcompiling 36eae17751 Update db.py just contain engine and create database. 2025-03-03 15:16:24 -05:00
imcompiling 7a57b75ca3 Add comments to models. 2025-03-03 14:41:24 -05:00
imcompiling 0ea9ef5b41 Rename sql_data_classes to db. 2025-03-03 14:29:50 -05:00
imcompiling e13790ba08 Add rest of docstrings. 2025-03-03 14:27:26 -05:00
imcompiling 171a1b0840 Move sql models into their own file. 2025-03-03 14:21:20 -05:00
renovate-bot 8604ba2547 Update ghcr.io/astral-sh/uv Docker tag to v0.6.3 2025-03-02 05:01:27 +00:00
imcompiling bd365d575f Add relationships. 2025-02-28 16:12:09 -05:00
imcompiling 0e262d5972 Add the rest of the tables as SQLModels. 2025-02-28 16:00:58 -05:00
imcompiling 774e6ffedf Add CategoryGroup definition.
Not working completely, the unique constraint is missing.
2025-02-27 16:28:00 -05:00
imcompiling de2542ece6 Start testing with sqlmodel. 2025-02-27 09:42:49 -05:00
imcompiling c29194a30a Install sqlmodel 2025-02-26 21:35:25 -05:00
imcompiling 8870d93a44 Fix quick code smell. 2025-02-26 20:42:44 -05:00
imcompiling 6a26440c4d Remove view for submit button.
build_develop / build (push) Successful in 38s
build_release / build (push) Successful in 22s
2025-02-26 20:26:42 -05:00
imcompiling 2735d6877f Remove messages intent. 2025-02-26 20:18:21 -05:00
imcompiling 117f5655d7 Merge pull request 'Update docker/login-action action to v3' (#75) from renovate/docker-login-action-3.x into main
build_develop / build (push) Successful in 20s
Reviewed-on: #75
Reviewed-by: imcompiling <[email protected]>
2025-02-25 09:28:16 -05:00
imcompiling c808bb2610 Merge branch 'main' into renovate/docker-login-action-3.x 2025-02-25 09:25:40 -05:00
imcompiling 5e7b3790eb Merge pull request 'Update ghcr.io/astral-sh/uv Docker tag to v0.6.2' (#74) from renovate/ghcr.io-astral-sh-uv-0.x into main
build_develop / build (push) Successful in 51s
Reviewed-on: #74
Reviewed-by: imcompiling <[email protected]>
2025-02-25 09:24:44 -05:00
renovate-bot 07ea150ad3 Update docker/login-action action to v3 2025-02-22 13:05:40 +00:00
renovate-bot 9280bbfbca Update ghcr.io/astral-sh/uv Docker tag to v0.6.2 2025-02-22 13:05:35 +00:00
imcompiling abf076fceb Remove exclude that isn't in this project.
build_develop / build (push) Successful in 36s
2025-02-21 14:05:30 -05:00
imcompiling 0c0997624f Remove guild ids.
build_develop / build (push) Successful in 39s
build_release / build (push) Successful in 21s
2025-02-20 20:29:16 -05:00
imcompiling 67090e0d6f Remove submission from table once handled by admins.
build_develop / build (push) Successful in 32s
build_release / build (push) Successful in 21s
2025-02-20 17:53:14 -05:00
imcompiling 385129c434 Fix smells in view.py. 2025-02-20 17:31:03 -05:00
imcompiling 327126307d Remove all references to table. Remove a dead function. Fix some code smells. 2025-02-20 17:23:57 -05:00
imcompiling 36dad683d8 Remove reference to boss_name. Fix a few smells. 2025-02-20 17:13:19 -05:00
imcompiling 9be9279239 Update the help setup command.
build_develop / build (push) Successful in 37s
build_release / build (push) Successful in 21s
2025-02-20 16:57:56 -05:00
imcompiling ffc5c20fec Add deprecation notes to SubmissionChannel. Keeping around just in case. 2025-02-20 16:50:10 -05:00
imcompiling 65453eb7d6 Edit verification view to use the database for information. 2025-02-20 16:48:18 -05:00
imcompiling 2ad1c808c2 Add a todo to rework how approval interacts with information. 2025-02-20 11:46:55 -05:00
imcompiling 3898e5e494 Fix types in database, add UserId, fix file usage in submission command. 2025-02-20 11:46:22 -05:00
imcompiling b16132d61b Add submission command to init cog. Clean up init cog. 2025-02-20 11:27:20 -05:00
imcompiling d3aaf642bd Add isinstance check for text channel. 2025-02-20 11:08:03 -05:00
imcompiling 1902c486fd Add the guilds constant for adding commands. 2025-02-18 23:35:30 -05:00
imcompiling 7e90e7c819 Add some pylance settings. Fix some pylance smells in submission.py. 2025-02-18 23:30:01 -05:00
imcompiling 279d3f4885 Set a primary key for the new table. Automap doesn't work without. 2025-02-18 23:05:02 -05:00
imcompiling 2c0f7f91c1 Add new logger for outputting to stdout. 2025-02-18 22:54:43 -05:00
imcompiling d699f8b3e7 Remove deprecated submission code. Address ruff smells. 2025-02-18 22:23:27 -05:00
imcompiling 56ced3f8a2 Remove message listener. Don't need bday prank anymore and the message listener for submissions has been removed due to command. 2025-02-18 22:17:42 -05:00
imcompiling ed4f8681c8 Remove the submission data storage class. Not needed anymore. 2025-02-18 22:15:03 -05:00
imcompiling d52008c536 Gut code from submission_views file. Only need the SubmissionCreateButton to tell users to use the new command. 2025-02-18 22:13:45 -05:00
imcompiling 8d0d0acc3b Add other files in .vscode dir. 2025-02-18 22:10:50 -05:00
imcompiling 9a1c3db7f9 Remove example config. This isn't used anymore. 2025-02-18 11:09:04 -05:00
imcompiling 96d78213ff Add vscode dir to source control. 2025-02-18 11:08:48 -05:00
imcompiling cdb54658ee Added new class for submission command. 2025-02-17 17:57:59 -05:00
imcompiling 4ce37ee3ea Add submissions table. 2025-02-17 17:40:03 -05:00
imcompiling c62b87c7f7 Apply fixed for ruff smells. 2025-02-17 16:39:10 -05:00
imcompiling ba04858328 Remove pylint stuff. 2025-02-17 16:08:29 -05:00
imcompiling a12fca6220 Address ruff smells in create.py. 2025-02-17 15:03:07 -05:00
imcompiling 136702654e Ran ruff check --fix. 2025-02-17 14:56:33 -05:00
imcompiling 23adad403a Merge branch '66-update-build-process'
build_develop / build (push) Successful in 56s
2025-02-17 14:50:36 -05:00
imcompiling 1d7952ed5a Remove depricated rule from ignore. 2025-02-17 14:49:52 -05:00
imcompiling ca54569033 Correct authors object in pyproject.toml. 2025-02-17 14:48:56 -05:00
imcompiling 3a72d9f0e7 Update renovatebot to not automatically merge minor dockerfile changes. 2025-02-17 14:45:08 -05:00
imcompiling da0acb2827 Merge pull request 'Update actions/checkout action to v4' (#72) from renovate/actions-checkout-4.x into main
build_develop / build (push) Successful in 48s
Reviewed-on: #72
2025-02-16 11:25:55 -05:00
renovate-bot 0baffe96f3 Update actions/checkout action to v4 2025-02-16 05:01:09 +00:00
imcompiling 5e6f5b2653 Merge branch 'main' into 66-update-build-process 2025-02-08 09:55:35 -05:00
imcompiling 990b7042c4 Add renovatebot json config.
build_develop / build (push) Successful in 49s
2025-02-04 17:38:07 -05:00
imcompiling aca05da68b Fix uv lock to not point to miso nexus. 2024-11-13 21:46:41 -05:00
imcompiling 0677d92c37 Bring lock file into source control. 2024-11-06 14:32:44 -05:00
imcompiling e7cfa26b4d Update docker ignore. 2024-11-06 14:16:01 -05:00
imcompiling 8960470a09 Update docker file to use uv to build image. 2024-11-06 13:43:28 -05:00
imcompiling 6006dceafa Added a docker ignore file. Don't need to copy over the venv. 2024-11-06 13:43:12 -05:00
imcompiling 1c9e92812e Add python version. 2024-11-06 13:40:59 -05:00
imcompiling d7ea690685 Update pyproject to use uv package manager. 2024-11-06 13:02:33 -05:00
imcompiling e85c6fe628 Move sql data classes into database setup directory
Edit docker file to install sqlite3.
2024-10-05 14:50:50 -04:00
imcompiling 48967675e2 Add setup tools as dependency. 2024-10-05 12:35:36 -04:00
imcompiling f5e54778d6 Update dependencies in pyproject to have specific version. 2024-10-05 12:33:45 -04:00
imcompiling 19f83ec2a5 run ruff format 2024-10-05 12:28:12 -04:00
imcompiling 7919a63e04 run ruff check --fix 2024-10-05 12:27:14 -04:00
imcompiling 00212ea09d Update dockerfile to use poetry. 2024-10-05 12:23:30 -04:00
imcompiling 96c902fcc3 Rename highscores.py to main.py 2024-10-05 12:23:16 -04:00
imcompiling c177ae67d6 Remove requirements. 2024-10-05 12:22:57 -04:00
imcompiling b73cddab53 Add ruff to project. 2024-10-05 11:52:28 -04:00
imcompiling 8f88c76e2e Add pyproject for poetry support. 2024-10-05 11:47:12 -04:00
imcompiling c977a4e958 Bug fix #64, was using wrong variable.
build_develop / build (push) Successful in 46s
Utils.py got formatted by ruff.
2024-09-30 12:55:34 -04:00
imcompiling e2e8d8de30 Don't need to print the help string.
build_develop / build (push) Successful in 1m20s
build_release / build (push) Successful in 20s
2024-09-29 23:31:49 -04:00
imcompiling 7f007ffa1f Merge commit '02e7a6df3a13bb19056f31f616101980b668ae43'
build_develop / build (push) Successful in 2m20s
2023-08-30 09:04:36 -04:00
imcompiling 913f915bf0 Clarify create category option description. 2023-08-30 09:04:24 -04:00
imcompiling 02e7a6df3a Comment out troublesome code. I believe that this causes users to have trouble submitting multiple in a row over 5 minutes. 2023-08-29 21:01:13 -04:00
imcompiling 7be869b2b6 Update registry location.
build_develop / build (push) Successful in 1m14s
2023-07-29 18:34:56 -04:00
imcompiling 15c4776918 its async. must await.
build_develop / build (push) Failing after 1m29s
2023-07-25 13:38:48 -04:00
imcompiling d3102b57cc Add new birthday action.
build_develop / build (push) Has been cancelled
2023-07-25 11:19:56 -04:00
imcompiling 9bef20de5b Clarify wording in submission process.
build
2023-04-30 21:54:02 -04:00
imcompiling 9231c04c6a Merge branch '61-guild-active-in-database'
build
2023-03-29 20:14:47 -04:00
imcompiling 36ba4d35e0 Update table definition. Add on_guild_remove listener. 2023-03-29 20:13:39 -04:00
imcompiling b80959866a Merge branch '44-limit-autofill-choices'
build
2023-03-29 19:19:20 -04:00
imcompiling de1eba025a Limit results from autofill functions.
Also define some types.
2023-03-29 19:19:04 -04:00
imcompiling 28e575f364 Remove docker-compose from repo.
build
2023-03-29 16:43:11 -04:00
imcompiling d8368b4b92 Merge branch '62-dm-on-join'
build
2023-03-29 16:42:41 -04:00
imcompiling cc10bb518c Add comment. 2023-03-29 16:42:25 -04:00
imcompiling 90131f6395 Add message to myself on join. Edit adding guild just in case they already exist in database. 2023-03-28 15:06:48 -04:00
imcompiling 1814867c09 Merge branch '58-guild-name-in-database'
build
2023-03-28 13:55:40 -04:00
imcompiling 390bf8a489 rename variable. 2023-03-28 13:55:18 -04:00
imcompiling 74aa78c748 Add loop for updating guild data. 2023-03-28 13:44:03 -04:00
imcompiling 379f280c11 Refactor presence to loops. 2023-03-28 13:02:07 -04:00
imcompiling 7d1e69c007 Don't need this import. 2023-03-28 12:56:50 -04:00
imcompiling b905ce7216 Change tag to v. 2023-03-28 12:51:39 -04:00
imcompiling 4bb7db3f71 Set push for specific tag only, set GIT_TAG env var. 2023-03-28 12:51:39 -04:00
imcompiling b3f48d4603 Correct on push. 2023-03-28 12:51:39 -04:00
imcompiling 90763c73a7 Add release workflow. 2023-03-28 12:51:39 -04:00
imcompiling 635419dc7c Change to operate on the main branch. No need for dev branch. 2023-03-28 12:51:39 -04:00
imcompiling 2356d5edbf Change workflow back to develop branch. 2023-03-28 12:51:39 -04:00
imcompiling 9eed0a1f4e Don't use action due to using local repo. 2023-03-28 12:51:39 -04:00
imcompiling a9a6ab4520 correct github address. 2023-03-28 12:51:39 -04:00
imcompiling e08a764591 Install docker onto image. 2023-03-28 12:51:39 -04:00
imcompiling 8b43f766b3 try https url 2023-03-28 12:51:39 -04:00
imcompiling cb9cad9783 maybe need buildx to set up docker 2023-03-28 12:51:39 -04:00
imcompiling 216becff6e try with docker commands instead of action. 2023-03-28 12:51:39 -04:00
imcompiling 836673198a try localhost for repo. 2023-03-28 12:51:39 -04:00
imcompiling e34739a0b1 Need buildx, don't understand why. 2023-03-28 12:51:39 -04:00
imcompiling 7c592e6acc correct http url, try again without buildx. 2023-03-28 12:51:39 -04:00
imcompiling 0f8637be23 try adding buildx to fix missing token. 2023-03-28 12:51:39 -04:00
imcompiling 3b597aeac7 delete unneeded files. 2023-03-28 12:51:39 -04:00
imcompiling c43b2297a4 Change runs-on, use action to build docker image. 2023-03-28 12:51:39 -04:00
imcompiling 1b337a291d add demo workflow 2023-03-28 12:51:39 -04:00
imcompiling 16b662875f Add build_develop workflow. 2023-03-28 12:51:39 -04:00
imcompiling 307f61934e Merge branch '59-gitea-action'
build
2023-03-23 16:52:15 -04:00
imcompiling 00265f2f58 Change tag to v. 2023-03-23 16:50:39 -04:00
imcompiling 93d558edbc Merge commit '0f2b1aa2e655a66c777a71c3917cd61147c9d754' into 59-gitea-action
build
2023-03-23 16:45:52 -04:00
imcompiling 0f2b1aa2e6 Set push for specific tag only, set GIT_TAG env var.
build
2023-03-23 16:44:08 -04:00
imcompiling 3f6bac99ab Merge branch '59-gitea-action'
build
2023-03-23 09:30:16 -04:00
imcompiling 46e2955642 Correct on push.
build
2023-03-23 09:30:00 -04:00
imcompiling ac9bf6b35f Merge branch 'develop' 2023-03-23 09:19:47 -04:00
imcompiling 869b2ccb82 Merge branch '59-gitea-action' into develop 2023-03-23 09:19:29 -04:00
imcompiling 3ddfdfe9b0 Add release workflow. 2023-03-23 09:16:01 -04:00
imcompiling 3860e74579 Change to operate on the main branch. No need for dev branch. 2023-03-23 09:09:41 -04:00
imcompiling 3b49a6ec2a Change workflow back to develop branch. 2023-03-22 23:25:26 -04:00
imcompiling 2f89588358 Don't use action due to using local repo.
build
2023-03-22 23:14:15 -04:00
imcompiling 0513df944f correct github address.
build
2023-03-22 22:31:42 -04:00
imcompiling ddf319b9d5 Install docker onto image.
build
2023-03-22 22:27:05 -04:00
imcompiling 13f5bc0aab try https url
build
2023-03-22 21:47:28 -04:00
imcompiling e6f3fcb604 maybe need buildx to set up docker
build
2023-03-22 19:35:11 -04:00
imcompiling eaa342d479 try with docker commands instead of action.
build
2023-03-22 19:34:27 -04:00
imcompiling ac11f3e462 try localhost for repo.
build
2023-03-22 19:22:18 -04:00
imcompiling 106b394c44 Need buildx, don't understand why.
build
2023-03-22 19:16:54 -04:00
imcompiling 6c1505685b correct http url, try again without buildx.
build
2023-03-22 19:15:15 -04:00
imcompiling 2e681be690 try adding buildx to fix missing token.
build
2023-03-22 19:12:03 -04:00
imcompiling 5448f2a426 delete unneeded files. 2023-03-22 19:11:22 -04:00
imcompiling 7647c355af Change runs-on, use action to build docker image.
Explore-GitHub-Actions
build
2023-03-22 18:56:20 -04:00
imcompiling a95fca40f9 add demo workflow
Explore-GitHub-Actions
build_develop
2023-03-21 22:06:46 -04:00
imcompiling 148ba253cf Add build_develop workflow.
build_develop
2023-03-21 21:53:04 -04:00
imcompiling 640d481018 Add guild name to database on join. 2023-03-21 21:41:51 -04:00
imcompiling 4c18513661 Merge branch '35-show-leaderboard-command' into develop 2023-03-11 15:12:59 -05:00
imcompiling 37b4d2d62e correct usage of format_score_string. Make format of view message better. 2023-03-11 15:12:36 -05:00
imcompiling 9c762fb346 Add on delete cascade to sql definition. 2023-03-10 17:56:49 -05:00
imcompiling 2c9e9e246a Create view leaderboard command. 2023-03-10 17:37:59 -05:00
imcompiling ab721c1625 create format_score_string function. 2023-03-10 16:27:40 -05:00
imcompiling 2864f342cc need to push tags separately. 2023-03-10 16:01:54 -05:00
imcompiling 92511db446 Merge branch '35-show-leaderboard-command' of https://gitea.cheezers.net/imcompiling/agon into 35-show-leaderboard-command 2023-03-10 12:18:25 -05:00
imcompiling 44c2720f80 Add new view file. 2023-03-10 12:17:58 -05:00
imcompiling 35cebdebce correct develop builder. 2023-03-10 11:57:28 -05:00
imcompiling e4437cc654 Correct the build scripts. 2023-03-10 11:56:45 -05:00
imcompiling fe2c400a2a Add build script. 2023-03-09 16:22:31 -05:00
imcompiling 39875585ef Change sqlalchemy version requirement. 2023-02-12 21:46:25 -05:00
imcompiling 7796ead644 Revert "Make command for admin only."
This reverts commit 8595a12f7f.
2023-02-12 21:37:12 -05:00
imcompiling 531d2f3a0e Merge commit '63e77958fb09c6849d48263994b69fcd92f46277' 2023-02-12 21:30:22 -05:00
imcompiling 8595a12f7f Make command for admin only. 2023-02-12 21:27:54 -05:00
imcompiling f76171ac84 Create simple help command for setting up. 2023-02-12 20:52:28 -05:00
imcompiling 1605c8ad50 Add new view file. 2023-02-12 13:33:05 -05:00
alwayscompiling 63e77958fb Do need to expose port for test bot. 2022-12-10 16:54:49 -05:00
alwayscompiling 0bae0ea5ff Remove custom mysqldb image, just use latest public. Move bot docker to base directory. 2022-12-10 16:12:56 -05:00
alwayscompiling c67e9ae617 Use pymysql for sql connection. 2022-12-10 16:12:21 -05:00
alwayscompiling 7e23ac14ff Create dockerfile for bot. Add bot to compose. 2022-12-10 12:46:12 -05:00
alwayscompiling a0a7e37921 Remove guild join stuff again. 2022-12-09 16:43:56 -05:00
alwayscompiling c01c888503 Merge branch '21-allow-users-to-modify-tablescategories' into develop 2022-12-08 23:17:21 -05:00
alwayscompiling 58dbe547bb Add submit button on group create. 2022-12-08 23:16:56 -05:00
alwayscompiling 3cf4fc6b33 Merge branch '21-allow-users-to-modify-tablescategories' into develop 2022-12-08 23:09:07 -05:00
alwayscompiling 411f8ab275 Add a deletion confirmation. 2022-12-08 22:59:00 -05:00
alwayscompiling 8952c4a352 Remove onjoin message. 2022-12-08 13:44:34 -05:00
alwayscompiling 93248797a0 Fill out rest of group and category command. Fill out doc strings. 2022-12-08 13:41:15 -05:00
alwayscompiling dace72f5cc Correct empty message id handling. 2022-12-08 13:28:13 -05:00
alwayscompiling f468a028d0 Changed send_highscore_message to take guild rather than channel. 2022-12-08 13:02:39 -05:00
alwayscompiling a9b889234a Start filling out create category. 2022-12-07 22:40:47 -05:00
alwayscompiling 88cec73644 Create command skeletons. Update set to conform to min and max values decided in create. 2022-12-07 22:24:38 -05:00
alwayscompiling 91e57c6555 Standardize autocomplete function names. 2022-12-07 21:52:36 -05:00
alwayscompiling 6c5f40f788 Refractor command files into single command files. 2022-12-07 21:46:04 -05:00
alwayscompiling a9920db381 Don't auto add runescape groups on join. 2022-12-07 20:54:49 -05:00
alwayscompiling d04b8c5e32 Squashed commit of the following:
commit 3627419e12
Merge: 2db744d 3446ce9
Author: alwayscompiling <[email protected]>
Date:   Tue Dec 6 22:36:51 2022 -0500

    Merge branch '45-sql-connection-timing-out' into develop

commit 3446ce9ca2
Author: alwayscompiling <[email protected]>
Date:   Tue Dec 6 22:27:05 2022 -0500

    Change session usage to on use basis.

commit 2db744d170
Author: alwayscompiling <[email protected]>
Date:   Sun Dec 4 21:44:54 2022 -0500

    Add restart policy.

commit fa33be1b4a
Author: alwayscompiling <[email protected]>
Date:   Sun Dec 4 21:25:57 2022 -0500

    Renamed docker compose file.
2022-12-06 23:02:01 -05:00
alwayscompiling 3627419e12 Merge branch '45-sql-connection-timing-out' into develop 2022-12-06 22:36:51 -05:00
alwayscompiling 3446ce9ca2 Change session usage to on use basis. 2022-12-06 22:28:53 -05:00
alwayscompiling 582b88e248 Create set command over Settings Management. 2022-12-04 22:42:55 -05:00
alwayscompiling 2db744d170 Add restart policy. 2022-12-04 21:44:54 -05:00
alwayscompiling fa33be1b4a Renamed docker compose file. 2022-12-04 21:25:57 -05:00
alwayscompiling 296e14f0f6 Add docker compose file. 2022-12-04 11:57:23 -05:00
alwayscompiling 4c8444d077 Move config file into utilities. 2022-12-04 11:00:22 -05:00
alwayscompiling 2e2030a533 Move mysql docker stuff into own dir. 2022-12-04 10:56:44 -05:00
alwayscompiling f4ec272567 Update functions for username length being in group. 2022-12-04 10:54:54 -05:00
alwayscompiling b7dd44a028 Change database structure. UsernameLength belong in CategoryGroup. 2022-12-03 20:56:57 -05:00
alwayscompiling 081e406d51 Allow user to update score. 2022-12-03 20:49:17 -05:00
alwayscompiling e3a937649f Fix string formatting for splitting categories into multiple rows. 2022-12-02 22:08:01 -05:00
alwayscompiling d09c9b2b03 Clean up username update verify message on approval. 2022-12-02 20:57:42 -05:00
alwayscompiling 8624285d09 Remove another subquery. Enforce username length. 2022-12-02 20:41:10 -05:00
alwayscompiling f1cbe81699 Missed incorrect subquery. 2022-12-02 18:29:23 -05:00
alwayscompiling aea2e0beb3 Remove subqueries when sending to IN call. 2022-12-02 18:27:50 -05:00
alwayscompiling fdd08bc1a2 bug fixes in from_group and autocomplete user. 2022-12-02 18:17:06 -05:00
alwayscompiling 98dbd30327 Only delete specified user. cat_name is already the string. 2022-12-02 17:49:11 -05:00
alwayscompiling 2565b7689f Correct arg names. 2022-12-02 17:42:59 -05:00
alwayscompiling 52305c0107 Clarify parameter name for users, correct id usage for autofills. 2022-12-02 17:02:44 -05:00
alwayscompiling bed07d440b Merge branch '13-use-a-database' of github.com:alwayscompiling/pvm-highscores into 13-use-a-database 2022-12-02 16:53:51 -05:00
alwayscompiling 4e832a084f Corrected autofill function return structures. 2022-12-02 16:32:22 -05:00
alwayscompiling cc20868bde Corrected autofill function return structures. 2022-12-02 16:08:36 -05:00
alwayscompiling 14602282e9 Clarify optional SlashOptions 2022-12-02 15:28:53 -05:00
alwayscompiling 2e3f298036 Correct score submission value. Parse times correctly. 2022-12-01 23:30:21 -05:00
alwayscompiling 44cb9025b0 Edit embeds to provide bot information for submission. 2022-12-01 23:26:04 -05:00
alwayscompiling 02145ba244 Create new python file for handling sqlalchemy. Removes circular import. 2022-12-01 22:58:39 -05:00
alwayscompiling 58e616ea47 Put pandas group into separate variable to remove warning. 2022-12-01 22:42:43 -05:00
alwayscompiling 2ec7296c0f Bug fixes from testing post_highscore_groups. Command name change. 2022-12-01 20:34:12 -05:00
alwayscompiling d0f328dd2d Variable name changes for clarification and fixing runtime issues. 2022-12-01 20:21:36 -05:00
alwayscompiling 2154d713cb Update username verify view to use new database. 2022-12-01 18:14:23 -05:00
alwayscompiling ea0ebcc2e9 Update data management to use new database. 2022-12-01 18:06:43 -05:00
alwayscompiling b9cc3b46bb Add autofill to get all users in guild. 2022-12-01 17:42:29 -05:00
alwayscompiling 439ad539a8 Updated message management to use new database. 2022-12-01 17:07:50 -05:00
alwayscompiling 10e8e5a446 Update settings management to new database. 2022-12-01 16:40:06 -05:00
alwayscompiling 34e55b962d Change autofill functions to return dicts of name id pairs. 2022-12-01 16:39:32 -05:00
alwayscompiling 1fdb9ff1b6 Update the verification view to use database. 2022-11-30 22:58:27 -05:00
alwayscompiling 4f255a32e2 Change submission to use database. Also use ids rather than names. 2022-11-30 22:36:45 -05:00
alwayscompiling 814baa27d6 Update message listener to new database. 2022-11-30 21:54:05 -05:00
alwayscompiling 9531dca892 Update guild listener to use new database. Add default config function to utils. 2022-11-30 21:45:46 -05:00
alwayscompiling 18df783d86 Update utils to use new database. 2022-11-30 20:54:06 -05:00
alwayscompiling 03d7485037 ignore the todo text file 2022-11-29 22:46:13 -05:00
alwayscompiling 1a55ae0ac5 Move category size to Categories table. 2022-11-29 21:11:44 -05:00
alwayscompiling 76d6ba7969 Change to sqlalchemy and orm scheme. Remove classes due to orm. Edit requirements. 2022-11-27 22:20:29 -05:00
alwayscompiling f95d3130ff Updating requirements. 2022-11-27 09:26:44 -05:00
alwayscompiling 09696071ef Correct guild id property name. 2022-11-27 00:43:24 -05:00
alwayscompiling e5d11d200c Update score to mainly hold sql query information. 2022-11-27 00:35:42 -05:00
alwayscompiling 6a87622b58 Update category class to just hold sql data. 2022-11-27 00:23:15 -05:00
alwayscompiling 9bc88a7e03 Do not need highscores_data class. 2022-11-27 00:07:42 -05:00
alwayscompiling 4c0e6a6209 Change table class to guild class. Change to just store sql table information. 2022-11-27 00:07:15 -05:00
alwayscompiling 017cb7c9df Update Guild class to hold sql Guilds table information. 2022-11-27 00:02:30 -05:00
alwayscompiling 17867671f1 Change order of database columns for easier reading. 2022-11-26 23:49:02 -05:00
alwayscompiling 8896e41726 Revert "Remove all data storage classes."
This reverts commit 77a32b3adc.
2022-11-26 23:20:35 -05:00
alwayscompiling 12a55dd626 Connect to db on bot start. Remove message map. 2022-11-26 21:36:38 -05:00
alwayscompiling 77a32b3adc Remove all data storage classes. 2022-11-26 20:59:44 -05:00
alwayscompiling 2f10a1c7e6 Final changes to correct primary key and uniqueness rules. 2022-11-26 20:52:54 -05:00
alwayscompiling a3bed52479 Remove password in dockerfile, change guild/channel ids to bigint unsigned. 2022-11-26 17:29:04 -05:00
alwayscompiling 6b7dd6042d Add files for docker sql server. 2022-11-25 14:15:55 -05:00
alwayscompiling c4d5477f8b Merge branch '1-add-more-administrative-commands' into develop 2022-11-24 17:55:06 -05:00
alwayscompiling f5342f1541 Fill functionality for delete user from_table and from_category. 2022-11-24 17:47:21 -05:00
alwayscompiling 1e336e290d Catching AttributeError in autofill functions when invalid variables are sent. 2022-11-24 17:45:40 -05:00
alwayscompiling 9ebbca2557 Fill out docstrings. 2022-11-24 17:05:45 -05:00
alwayscompiling 8a2108fc14 Create shell commands and autofill functions. Fix set initializer in utils.autofill_users. 2022-11-22 17:12:26 -05:00
alwayscompiling 03c7a9303a Small rewrite to consolidate autofill_user function. Sort imports in message_management.py. 2022-11-22 17:02:18 -05:00
alwayscompiling a0c5527bf6 Create autofill functions for users in category and in table. Add function in category class to return a list of all users. 2022-11-22 16:26:31 -05:00
alwayscompiling 8155fb0c8f Reorganized commands to facilitate new commands. 2022-11-22 16:01:55 -05:00
alwayscompiling 06b46ef27c Create general autofill table and category util functions. Update table class to have get categories function. 2022-11-17 14:50:48 -05:00
alwayscompiling 1858f0c37d Add refresh table command. 2022-11-16 16:30:34 -05:00
alwayscompiling 792a225987 Correct docstring 2022-11-15 16:17:22 -05:00
alwayscompiling 2e324dc9c5 Use the new category pad length variable. 2022-11-15 15:52:58 -05:00
alwayscompiling 09f7a4323c Correct category pad default. 2022-11-15 15:50:12 -05:00
alwayscompiling dd73e9b472 Add commands to edit username length and category pad. 2022-11-15 15:48:17 -05:00
alwayscompiling b4f5ddc822 Update to nextcord 2.3.2. 2022-11-12 01:16:04 -05:00
alwayscompiling d2f33959ed Create remove_user command. 2022-08-13 21:46:41 -04:00
alwayscompiling 652533d071 Should have handled username updating code in the command, didn't need to create new functions for the command. 2022-08-13 21:26:50 -04:00
alwayscompiling 73e350c845 Save takes guild object, not id. 2022-08-13 20:46:59 -04:00
alwayscompiling 9dec15dbfa Correct table update_username. 2022-08-13 20:45:29 -04:00
alwayscompiling 41ba4dc23c Save data after modified. 2022-08-13 20:40:06 -04:00
alwayscompiling 790cff3406 Clear view on approval. 2022-08-13 20:33:14 -04:00
alwayscompiling 7fb87ae12f Add username setter. 2022-08-13 20:30:21 -04:00
alwayscompiling 2407e61913 Correcting embed access. 2022-08-13 20:30:03 -04:00
alwayscompiling 6147263647 Add UpdateUsernameVerify view to MessageManagementCog. 2022-08-13 20:23:56 -04:00
alwayscompiling cb71cbe1d5 Create update_username functions for category and table. 2022-08-13 20:08:57 -04:00
alwayscompiling 080356fe9c Create Guild.update_username(). 2022-08-13 19:48:29 -04:00
alwayscompiling d4e19284f0 Create view for verifyng username update request. 2022-08-12 23:18:21 -04:00
alwayscompiling ae9153b78e Change submission views to be more informative. 2022-08-12 22:49:54 -04:00
alwayscompiling ca58e9ac26 Create initial update_username command. 2022-08-12 22:39:12 -04:00
alwayscompiling 432888e468 Tag user on submission verify. 2022-08-12 22:28:14 -04:00
alwayscompiling c918b9492b Renamed to data_management. Better description. 2022-08-11 23:02:44 -04:00
alwayscompiling caef6a3652 Created command to clear table messages. 2022-08-11 23:00:11 -04:00
alwayscompiling b2acafd6e7 Changed design to be one class per file. 2022-08-11 22:44:22 -04:00
alwayscompiling 349fdfe18d Renamed python files to more describe their contents. Intend on having more than 1 class per file. 2022-08-11 22:17:52 -04:00
alwayscompiling efed6e001f Moved all commands into single commands cog. 2022-08-11 21:55:06 -04:00
alwayscompiling 6b0e61cdf7 Update nextcord. 2022-08-08 22:12:41 -04:00
alwayscompiling 0acfa77492 Change delete_scores to a slash command. 2022-08-08 22:07:12 -04:00
alwayscompiling ffefd08761 Updated presense to account for slash commands. 2022-08-08 21:54:27 -04:00
alwayscompiling a61662429a Remove help command. Doesn't work with slash commands. 2022-08-08 21:51:25 -04:00
alwayscompiling 57244bcbfe Update post highscores command to slash command. 2022-08-08 21:51:06 -04:00
alwayscompiling 3fd9ff46a3 imports cleanup. 2022-08-08 21:36:45 -04:00
alwayscompiling d4ad727f2a Correcting error statement. 2022-08-06 22:58:43 -04:00
alwayscompiling 516ff7feeb Update minimum python version. Nextcord v2.0 requires python 3.8. 2022-08-06 22:26:55 -04:00
alwayscompiling 0eb332aefd Added verify and submission register subcommands. Removed bot argument from Register class. 2022-08-06 22:21:08 -04:00
alwayscompiling ff07787a9a Create register group and highscore slash subcommand. 2022-08-06 20:14:30 -04:00
62 changed files with 3788 additions and 2457 deletions
+5
View File
@@ -0,0 +1,5 @@
.*
Dockerfile
LICENSE
README.md
renovate.json
+32
View File
@@ -0,0 +1,32 @@
name: build_develop
on:
push:
branches:
- "*"
- "!main"
paths-ignore:
- "README.md"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Log into container registry
uses: docker/login-action@v3
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: set env
run: echo "GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
- name: Build Docker Image
run: docker build -t gitea.cheezers.org/imcompiling/agon:${{env.GIT_BRANCH}} .
- name: Push Docker Image
run: docker push gitea.cheezers.org/imcompiling/agon:${{env.GIT_BRANCH}}
+28
View File
@@ -0,0 +1,28 @@
name: build_latest
on:
push:
branches: [main]
paths-ignore:
- "README.md"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Log into container registry
uses: docker/login-action@v3
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: Build Docker Image
run: docker build -t gitea.cheezers.org/imcompiling/agon:latest .
- name: Push Docker Image
run: docker push gitea.cheezers.org/imcompiling/agon:latest
+28
View File
@@ -0,0 +1,28 @@
name: build_release
on:
push:
tags: ["v[0-9].[0-9].[0-9]"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Log into container registry
uses: docker/login-action@v3
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: Set env
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Build Docker Images
run: docker build -t gitea.cheezers.org/imcompiling/agon:${{env.GIT_TAG}} .
- name: Push Tagged Image
run: docker push gitea.cheezers.org/imcompiling/agon:${{env.GIT_TAG}}
+5 -4
View File
@@ -105,6 +105,7 @@ venv/
ENV/
env.bak/
venv.bak/
poetry.lock
# Spyder project settings
.spyderproject
@@ -124,9 +125,6 @@ dmypy.json
# Pyre type checker
.pyre/
# IDE
.vscode/
# logging
nextcord.log
@@ -134,4 +132,7 @@ nextcord.log
highscores_data.json
highscores_config.json
deleted_backups/
highscores_data/
highscores_data/
TODO.txt
sql_tester.py
/database
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"kevinrose.vsc-python-indent",
"charliermarsh.ruff",
"donjayamanne.python-extension-pack"
]
}
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Module",
"type": "debugpy",
"request": "launch",
"module": "agon",
"justMyCode": true
}
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"cSpell.words": [
"dotenv",
"gmtime",
"highscore",
"highscores",
"imcompiling",
"iterrows",
"nextcord",
"ondelete"
],
"python.analysis.diagnosticSeverityOverrides": {
"reportOptionalMemberAccess": "warning",
"reportArgumentType": "information",
"reportAttributeAccessIssue": "warning"
}
}
+15
View File
@@ -0,0 +1,15 @@
FROM python:3.14.8-slim
# install uv
COPY --from=ghcr.io/astral-sh/uv:0.9.18 /uv /uvx /bin/
RUN apt-get update && apt-get -y upgrade && apt-get -y install sqlite3
# Copy project to image
ADD . /home
# Sync the project into a new environment, using the frozen lockfile
WORKDIR /home
RUN uv sync --frozen --no-dev
CMD ["uv", "run", "--frozen", "--no-dev", "-m", "agon"]
+1
View File
@@ -0,0 +1 @@
"""Empty init file for off_cooldown_bot module."""
+14
View File
@@ -0,0 +1,14 @@
"""Starts the app for agon."""
import logging
from .app import run
nextcord_logger = logging.getLogger("nextcord")
nextcord_logger.setLevel(logging.DEBUG)
nextcord_handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
nextcord_handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
nextcord_logger.addHandler(nextcord_handler)
if __name__ == "__main__":
run()
+42
View File
@@ -0,0 +1,42 @@
"""Main file for Agon bot."""
import pathlib
import sys
import nextcord
from nextcord.ext import commands
from . import config
from .logger import agon_logger
def run() -> None:
"""Main function for bot."""
min_python_version = (3, 12)
if sys.version_info < min_python_version:
agon_logger.error("ERROR: Python version must be >= 3.12")
sys.exit()
# Allows privileged intents for monitoring members joining, roles editing, and role assignments
# These need to be enabled in the developer portal as well
intents = nextcord.Intents.default()
bot = commands.Bot(
commands.when_mentioned_or(config.BOT_PREFIX),
intents=intents,
)
# Get the modules of all cogs whose directory structure is ./cogs/<module_name>
for folder in pathlib.Path("./agon/cogs").iterdir():
bot.load_extension(f"agon.cogs.{folder.name}")
@bot.listen()
async def on_ready() -> None:
"""When Discord is connected."""
if bot.user is None:
agon_logger.error("Discord connection failed.")
return
agon_logger.info("%s has connected to Discord!", bot.user.name)
# Run Discord bot
bot.run(config.DISCORD_TOKEN)
+28
View File
@@ -0,0 +1,28 @@
"""Cog loader. Adds all command cogs to the bot."""
from nextcord.ext import commands
from agon.cogs.commands.create import CreateCog
from agon.cogs.commands.delete import DeleteCog
from agon.cogs.commands.get import GetCog
from agon.cogs.commands.help import HelpCog
from agon.cogs.commands.refresh import RefreshCog
from agon.cogs.commands.set import SetCog
from agon.cogs.commands.submission import SubmissionCog
from agon.cogs.commands.update import UpdateCog
from agon.cogs.commands.view import ViewCog
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot) -> None:
"""Adds all command classes to bot."""
bot.add_cog(CreateCog(bot))
bot.add_cog(DeleteCog())
bot.add_cog(GetCog())
bot.add_cog(HelpCog())
bot.add_cog(RefreshCog(bot))
bot.add_cog(SetCog())
bot.add_cog(SubmissionCog())
bot.add_cog(UpdateCog(bot))
bot.add_cog(ViewCog())
+157
View File
@@ -0,0 +1,157 @@
"""Commands for creating items in bot."""
import nextcord
from nextcord.ext import commands
from sqlalchemy import exc, select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Category, CategoryGroup
from agon.utils import MAX_LINE_LENGTH, autofill_groups, send_highscore_message
class CreateCog(commands.Cog, name="Create Cog"):
"""Class holds commands for creating things."""
def __init__(self, bot: commands.Bot) -> None:
"""Default constructor for CreateCog class."""
self._bot = bot
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def create(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for creating things. Never called."""
@create.subcommand(description="Create a new Category. Must not exist already in Group.")
async def category( # noqa: PLR0913 need all these arguments because discord command.
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to add Category to.", required=True),
category_name: str = nextcord.SlashOption(description="Name of new Category.", required=True),
is_ascending: bool = nextcord.SlashOption(description="If lower value is a better score.", required=True), # noqa: FBT001
is_time_record: bool = nextcord.SlashOption( # noqa: FBT001
description="If the score should be shown in time format. Cannot be changed.", required=True
),
is_validated: bool = nextcord.SlashOption( # noqa: FBT001
description="If scores need to be validated before posted.", required=True
),
size: int = nextcord.SlashOption(
description="How many scores to post in Category. Default is 3.", required=False, default=3, min_value=1
),
) -> None:
"""
Creates a new category to be placed into group_id group.
Fails if the category name already exists within the group. Then calls send_highscore_message().
Args:
interaction (nextcord.Interaction): Interaction for the command.
group_id (str): Id of group to add category to. Is SlashOption.
category_name (str): Name of new category. Is SlashOption.
is_ascending (bool): If the scores are sorted ascending or not. Is SlashOption.
is_time_record (bool): If the scores are a time record or not. Is SlashOption.
is_validated (bool): If the scores should be validated or not. Is SlashOption.
size (int): How many sores to show in the category message. Is SlashOption.
"""
new_category = Category(
name=category_name,
group_id=int(group_id),
size=size,
is_ascending=is_ascending,
is_time_record=is_time_record,
is_validated=is_validated,
)
# Open connection to engine and add new category.
with Session(engine) as session:
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt)
try:
session.add(new_category)
session.commit()
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'"{category_name}" has been added to "{group.name}"!', ephemeral=True
)
except exc.IntegrityError:
# Category name already exists in group
session.rollback()
await interaction.response.send_message(
f'"{category_name}" already exists in "{group.name}". Please rename.', ephemeral=True
)
@create.subcommand(description="Create a new Category Group. Must not exist already for Guild.")
async def group(
self,
interaction: nextcord.Interaction,
group_name: str = nextcord.SlashOption(description="Name of new Group.", required=True),
pad_length: int = nextcord.SlashOption(
description="Number of characters wide a category is printed. Default is 26.",
required=False,
default=26,
min_value=7,
max_value=MAX_LINE_LENGTH,
),
username_length: int = nextcord.SlashOption(
description="Number of characters of a username to display. Default is 12.",
required=False,
default=12,
min_value=1,
max_value=30,
),
channel: nextcord.TextChannel | nextcord.Thread | None = nextcord.SlashOption(
description="Channel to post Group in. If left empty, defaults to current channel.",
required=False,
default=None,
),
) -> None:
"""
Creates a new category group to be placed into the database with the guild's id.
Fails if the category group name already exists within the guild. Then calls send_highscore_message().
Args:
interaction (nextcord.Interaction): Interaction for the command.
group_name (str): Name of new Group. Is SlashOption.
pad_length (int): Number of characters wide a category is printed. Is SlashOption.
username_length (int): Number of characters of a username to display. Is SlashOption.
channel (Optional[nextcord.abc.GuildChannel]): Channel to post group in. If None, defaults to current
channel. Is SlashOption.
"""
# if no channel provided, use current channel.
if channel is None:
channel_id: int = interaction.channel.id
else:
channel_id: int = channel.id
new_group = CategoryGroup(
name=group_name,
channel_id=channel_id,
guild_id=interaction.guild_id,
pad_length=pad_length,
username_length=username_length,
)
with Session(engine) as session:
try:
session.add(new_group)
session.commit()
await send_highscore_message(session, interaction.guild, new_group)
await interaction.response.send_message(f'"{group_name}" has been created!', ephemeral=True)
except exc.IntegrityError:
# Category Group already exists in guild
session.rollback()
await interaction.response.send_message(
f'"{group_name}" already exists in "{interaction.guild.name}". Please rename.', ephemeral=True
)
@category.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
"""
Collects list of group names and compares to input group name for autocomplete assistance.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocomplete compared to.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
+288
View File
@@ -0,0 +1,288 @@
"""Commands for deleting groups/categories/scores in database and messages in discord."""
import nextcord
from nextcord.ext import commands
from sqlalchemy import distinct, select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Category, CategoryGroup, Score
from agon.utils import (
autofill_categories,
autofill_groups,
autofill_users_in_group_category,
autofill_users_in_guild,
send_highscore_message,
)
from .views.delete_confirm import DeleteConfirmView, create_delete_id
class DeleteCog(commands.Cog, name="Delete Cog"):
"""Class holds commands for deleting."""
def __init__(self) -> None:
"""Default constructor for DeleteCog class."""
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def delete(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for deleting things. Never called."""
@delete.subcommand()
async def all(self, interaction: nextcord.Interaction) -> None:
"""Subcommand parent for deleting all of a thing. Never called."""
@all.subcommand(description="Clears highscores channel of all highscore messages. Does not change data.")
async def channel_messages(
self,
interaction: nextcord.Interaction,
channel: nextcord.TextChannel | nextcord.Thread | None = nextcord.SlashOption(
description="Channel to clear of messages.",
required=False,
default=None,
),
) -> None:
"""
Deletes all group messages from channel. Does not change data.
Does not delete highscores channel id. Uses current channel if not supplied a channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (Optional[nextcord.abc.GuildChannel]): Channel to clear of messages.
Is a SlashOption. Limited to text channels. Defaults to None.
"""
if not channel:
if isinstance(interaction.channel, nextcord.TextChannel | nextcord.Thread):
channel = interaction.channel
else:
# This should never happen.
await interaction.response.send_message("Channel is not a text channel somehow.", ephemeral=True)
return
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(col(CategoryGroup.channel_id) == channel.id)
)
# TODO test when nothing in channel
groups = session.scalars(stmt)
await interaction.response.defer(ephemeral=True)
for group in groups:
if group.message_id is not None:
try:
message: nextcord.Message = await channel.fetch_message(group.message_id)
await message.delete()
except nextcord.NotFound:
# silently ignore that messages are not found. They got deleted already.
pass
await interaction.send("Highscore messages deleted.", ephemeral=True)
@all.subcommand(
description="Removes all of a user's scores. Warning: data is destroyed, use with care.",
)
async def users(
self,
interaction: nextcord.Interaction,
user_name: str = nextcord.SlashOption(
description="User to remove scores for. Note: This is not always a user's discord name.", required=True
),
) -> None:
"""
Searches all of guild's scores data for any score from the user and removes.
Updates highscore message if user was removed from that Category Group.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
user_name (str): User to remove from highscores
"""
with Session(engine) as session:
stmt = select(distinct(col(Score.category_id))).where(col(Score.username) == user_name)
category_ids: list[int] = list(session.scalars(stmt))
stmt = select(Score).where(col(Score.username) == user_name)
for score in session.scalars(stmt):
session.delete(score)
session.commit()
group_ids = select(col(Category.group_id)).where(col(Category.id).in_(category_ids))
stmt = select(CategoryGroup).where(col(CategoryGroup.id).in_(group_ids))
for group in session.scalars(stmt):
await send_highscore_message(session, interaction.guild, group)
await interaction.send(f'User "{user_name}" was removed from all Category Groups.', ephemeral=True)
@delete.subcommand(description="Delete a category from a group. Deletion is permanent.")
async def category(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete from.", required=True),
category_id: str = nextcord.SlashOption(name="category", description="Category to delete.", required=True),
) -> None:
"""
Calls the delete confirmation view for the given group and category.
Args:
interaction (nextcord.Interaction): Interaction of command.
group_id (str): Group to delete from. Is SlashOption.
category_id (str): Category to delete. Is SlashOption.
"""
await interaction.response.send_modal(
DeleteConfirmView(custom_id=create_delete_id(int(group_id), int(category_id)))
)
@delete.subcommand(description="Delete a group. Deletion is permanent.")
async def group(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete.", required=True),
) -> None:
"""
Calls the delete confirmation view for the given group.
Args:
interaction (nextcord.Interaction): Interaction of command.
group_id (str): Group to delete from. Is SlashOption.
"""
await interaction.response.send_modal(DeleteConfirmView(custom_id=create_delete_id(int(group_id))))
@delete.subcommand()
async def user(self, interaction: nextcord.Interaction) -> None:
"""Subcommand parent for deleting user information. Never called."""
@user.subcommand(description="Deletes a user's scores from a category.")
async def from_category(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete user from.", required=True),
category_id: str = nextcord.SlashOption(
name="category", description="Category to delete user from.", required=True
),
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
) -> None:
"""
Removes a user from a category. Uses autofill functions to fill out group_id, category_id, and user_name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to remove user from. Is SlashOption.
category_id (str): String representation of Category id to remove user from. Is SlashOption.
user_name (str): The user to remove. Is SlashOption.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt)
stmt = select(col(Category.name)).where(col(Category.id) == int(category_id))
category_name = session.scalar(stmt)
stmt = (
select(Score).where(col(Score.category_id) == int(category_id)).where(col(Score.username) == user_name)
)
# there should only be one because category id and username are a composite primary key in score
session.delete(session.scalar(stmt))
session.commit()
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'User "{user_name}" removed from "{category_name}".', ephemeral=True
)
@user.subcommand(description="Deletes a user's scores from entire group.")
async def from_group(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to delete user from.", required=True),
user_name: str = nextcord.SlashOption(description="User to delete.", required=True),
) -> None:
"""
Removes user from a group. Uses autofill functions to fill out group_id and user_name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to remove user from. Is SlashOption.
user_name (str): The user to remove. Is SlashOption.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt)
category_ids = select(col(Category.id)).where(col(Category.group_id) == int(group_id))
stmt = select(Score).where(col(Score.category_id).in_(category_ids)).where(col(Score.username) == user_name)
for score in session.scalars(stmt):
session.delete(score)
session.commit()
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'User "{user_name}" removed from all categories in {group.name}.', ephemeral=True
)
@from_category.on_autocomplete("group_id")
@from_group.on_autocomplete("group_id")
@category.on_autocomplete("group_id")
@group.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
"""
Autocomplete function for group names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String to be autocompleted with Group name.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
@from_category.on_autocomplete("category_id")
@category.on_autocomplete("category_id")
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
"""
Autocomplete function for category names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
category_id (str): String to be autocompleted with Category name.
group_id (str): Group to find Category from.
"""
await interaction.response.send_autocomplete(
autofill_categories(interaction.guild_id, int(group_id), category_id)
)
@from_category.on_autocomplete("user_name")
@from_group.on_autocomplete("user_name")
async def user_autocomplete(
self, interaction: nextcord.Interaction, user_name: str, group_id: str, category_id: str | None = None
) -> None:
"""
Autocomplete functions for user names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
user_name (str): String to be autocompleted with User name.
group_id (str): String representation of Group id to find category from.
category_id (Optional[str]): String representation of Category id to find User from, can be None.
"""
await interaction.response.send_autocomplete(
autofill_users_in_group_category(
interaction.guild_id, int(group_id), int(category_id) if category_id else 0, user_name
)
)
@users.on_autocomplete("user_name")
async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str) -> None:
"""
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
Args:
interaction (nextcord.Interaction): Interaction with command.
username (str): String to run autocompletion against.
"""
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
+199
View File
@@ -0,0 +1,199 @@
"""Get command hierarchy."""
import nextcord
from nextcord.ext import commands
from sqlmodel import Session, col, select
from agon.database.db import engine
from agon.database.models import Category, CategoryGroup
from agon.utils import autofill_categories, autofill_groups
class GetCog(commands.Cog, name="Get command."):
"""Class holds commands for setting variables in guild's database."""
def __init__(self) -> None:
"""Default constructor for SetCog class. Does nothing."""
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def get(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for getting. Never called."""
@get.subcommand(description="Commands for getting category information.")
async def category(self, interaction: nextcord.Interaction) -> None:
"""Parent slash subcommand for getting category information. Never called."""
@category.subcommand(description="Get whether a category is sorted in ascending or descending order.")
async def is_ascending(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group that Category is in.", required=True),
category_id: str = nextcord.SlashOption(
name="category", description="Category to information from.", required=True
),
) -> None:
"""
Command to view how a category is sorted.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id get information from. Is SlashOption.
category_id (str): String representation of Category id to get information from. Is SlashOption.
"""
stmt = (
select(Category).where(col(Category.group_id) == int(group_id)).where(col(Category.id) == int(category_id))
)
with Session(engine) as session:
if (category := session.scalar(stmt)) is None:
await interaction.response.send_message("Group or Category was not found.", ephemeral=True)
return
await interaction.response.send_message(
f"**{category.name}** is sorted in **{'ascending' if category.is_ascending else 'descending'}** order.",
ephemeral=True,
)
@category.subcommand(description="Get whether a category required validation.")
async def is_validated(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group that Category is in.", required=True),
category_id: str = nextcord.SlashOption(
name="category", description="Category to information from.", required=True
),
) -> None:
"""
Command to view if a category required photo validation.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id get information from. Is SlashOption.
category_id (str): String representation of Category id to get information from. Is SlashOption.
"""
stmt = (
select(Category).where(col(Category.group_id) == int(group_id)).where(col(Category.id) == int(category_id))
)
with Session(engine) as session:
if (category := session.scalar(stmt)) is None:
await interaction.response.send_message("Group or Category was not found.", ephemeral=True)
return
await interaction.response.send_message(
f"{category.name} does {'not ' if not category.is_validated else ''} require validation.",
ephemeral=True,
)
@category.subcommand(description="Get the size for a category.")
async def size(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group that Category is in.", required=True),
category_id: str = nextcord.SlashOption(name="category", description="Category to get size of.", required=True),
) -> None:
"""
Command to view a category's size.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id get information from. Is SlashOption.
category_id (str): String representation of Category id to get information from. Is SlashOption.
"""
stmt = (
select(Category).where(col(Category.group_id) == int(group_id)).where(col(Category.id) == int(category_id))
)
with Session(engine) as session:
if (category := session.scalar(stmt)) is None:
await interaction.response.send_message("Group or Category was not found.", ephemeral=True)
return
await interaction.response.send_message(
f"**{category.name}** has a size of **{category.size}**.", ephemeral=True
)
@get.subcommand(description="Commands for getting group information.")
async def group(self, interaction: nextcord.Interaction) -> None:
"""Parent slash subcommand for getting group information. Never called."""
@group.subcommand(description="Get the pad length of a group.")
async def pad_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to get pad length of.", required=True),
) -> None:
"""
Command to view a group's pad length.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id get information from. Is SlashOption.
"""
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
with Session(engine) as session:
if (group := session.scalar(stmt)) is None:
await interaction.response.send_message("Group was not found.", ephemeral=True)
return
await interaction.response.send_message(
f"**{group.name}** has a pad length of **{group.pad_length}**.", ephemeral=True
)
@group.subcommand(description="Get the username length of a group.")
async def username_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Group to get username length of.", required=True
),
) -> None:
"""
Command to view a group's username length.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id get information from. Is SlashOption.
"""
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
with Session(engine) as session:
if (group := session.scalar(stmt)) is None:
await interaction.response.send_message("Group was not found.", ephemeral=True)
return
await interaction.response.send_message(
f"**{group.name}** has a username length of **{group.username_length}**.", ephemeral=True
)
@is_ascending.on_autocomplete("category_id")
@is_validated.on_autocomplete("category_id")
@size.on_autocomplete("category_id")
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
"""
Autocomplete function for category names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
category_id (str): String to be autocompleted with Category name.
group_id (str): Group to find Category from.
"""
await interaction.response.send_autocomplete(
autofill_categories(interaction.guild_id, int(group_id), category_id)
)
@is_ascending.on_autocomplete("group_id")
@is_validated.on_autocomplete("group_id")
@pad_length.on_autocomplete("group_id")
@size.on_autocomplete("group_id")
@username_length.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
"""
Sends results from autofill_groups back to group_output_channel for autocompletion of group_id.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocompletion against.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
+35
View File
@@ -0,0 +1,35 @@
"""Commands for helping a user."""
import nextcord
from nextcord.ext import commands
class HelpCog(commands.Cog, name="Help commands."):
"""Class holds commands for getting help."""
def __init__(self) -> None:
"""Default constructor for HelpCog class. Does nothing."""
@nextcord.slash_command()
async def help(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for help. Never called."""
@help.subcommand(description="Describes how to get started with Agon.")
async def setup(self, interaction: nextcord.Interaction) -> None:
"""
Sends a message describing how to get started with Agon.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
# I hate this but don't want indents in the string.
help_str: str = "Welcome to Agon! Three simple steps are needed to get started."
help_str += '1.) Set a channel to receive submission verifications with "/set verify_channel."\n'
help_str += "This channel is used to allow moderators to check if a submission is real/valid.\n\n"
help_str += '2.) Set up your highscores! Use "/create group" to first create a group message in the'
help_str += 'channel you would like and then use "/create category" to create a category in that group message!'
help_str += "\n\n"
help_str += '3.) Users can then submit scores with the "/submit" command.'
await interaction.response.send_message(content=help_str)
+89
View File
@@ -0,0 +1,89 @@
"""Commands for refreshing highscore messages."""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import CategoryGroup
from agon.utils import autofill_groups, send_highscore_message
class RefreshCog(commands.Cog, name="Refresh Cog"):
"""Class holds commands for refreshing."""
def __init__(self, bot: commands.Bot) -> None:
"""
Default constructor for RefreshCog class.
Args:
bot (commands.Bot): Bot object. Required to register submission button view.
"""
self._bot = bot
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def refresh(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for refreshing things. Never called."""
@refresh.subcommand(description="Manually refresh a group. Use when changing settings or data manually changed.")
async def group(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Name of Group.", required=True),
) -> None:
"""
Refreshes a group given by user.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): Str representation of Group id to refresh. Is SlashOption.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(col(CategoryGroup.id) == int(group_id))
)
group = session.scalar(stmt)
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(f"Updated group {group.name}!", ephemeral=True)
@refresh.subcommand()
async def all(self, interaction: nextcord.Interaction) -> None:
"""Subcommand parent for refreshing all things. Never called."""
@all.subcommand(description="Triggers post/update of highscores to registered highscore channels.")
async def groups(self, interaction: nextcord.Interaction) -> None:
"""
A command which posts the highscore in the registered channel.
Sends warning if no channel has been registered. Either creates new messages for each group if one doesn't
exist or edits the existing message.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(col(CategoryGroup.guild_id) == interaction.guild_id)
# awaiting response because posting scores takes a while
await interaction.response.defer(ephemeral=True)
for group in session.scalars(stmt):
await send_highscore_message(session, interaction.guild, group)
await interaction.send(f"Posted all groups in {interaction.guild.name}'s highscores.", ephemeral=True)
@group.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
"""
Collects list of group names and compares to input group name for autocomplete assistance.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocomplete compared to.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
+449
View File
@@ -0,0 +1,449 @@
"""Set command hierarchy."""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Category, CategoryGroup, Guild
from agon.logger import agon_logger
from agon.utils import MAX_LINE_LENGTH, autofill_categories, autofill_groups, send_highscore_message
class SetCog(commands.Cog, name="Set command."):
"""Class holds commands for setting variables in guild's database."""
def __init__(self) -> None:
"""Default constructor for SetCog class. Does nothing."""
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def set(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for setting settings. Never called."""
@set.subcommand(description="Commands for editing a category.")
async def category(self, interaction: nextcord.Interaction) -> None:
"""Parent slash subcommand for setting category settings. Never called."""
@category.subcommand(description="Update the number of entries to show for a category.", name="size")
async def category_size(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group that Category is in.", required=True),
category_id: str = nextcord.SlashOption(
name="category", description="Category to update size of.", required=True
),
size: int = nextcord.SlashOption(description="Number of entries to show for the category.", required=True),
) -> None:
"""
Command to update a category's size.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to register channel to. Is SlashOption.
category_id (str): String representation of Category id to register channel to. Is SlashOption.
size (int): Size to set. Is SlashOption.
"""
stmt = (
select(Category).where(col(Category.group_id) == int(group_id)).where(col(Category.id) == int(category_id))
)
with Session(engine) as session:
if (category := session.scalar(stmt)) is None:
await interaction.response.send_message("Category or Group was not found.", ephemeral=True)
return
category.size = size
session.commit()
# Use the relationship to get group category is in.
if (group := category.category_group) is None:
agon_logger.warning(f"{category.name}:{category.id} group relationship was None.")
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == group_id)
if (group := session.scalar(stmt)) is None:
await interaction.response.send_message("Group was not found.", ephemeral=True)
return
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f"**{category.name}** size has been updated to **{size}**.", ephemeral=True
)
@category.subcommand(description="Update the is_ascending value for a category.")
async def is_ascending(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group that Category is in.", required=True),
category_id: str = nextcord.SlashOption(
name="category", description="Category to update is_ascending of.", required=True
),
ascending: bool = nextcord.SlashOption( # noqa: FBT001
description="Whether the scores are sorted ascending or not.", required=True
),
) -> None:
"""
Command to update a category's is_ascending boolean.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to register channel to. Is SlashOption.
category_id (str): String representation of Category id to register channel to. Is SlashOption.
ascending (bool): Boolean to set. Determines whether scores are sorted ascending or not. Is SlashOption.
"""
stmt = (
select(Category).where(col(Category.group_id) == int(group_id)).where(col(Category.id) == int(category_id))
)
with Session(engine) as session:
if (category := session.scalar(stmt)) is None:
await interaction.response.send_message("Category or Group was not found.", ephemeral=True)
return
category.is_ascending = ascending
session.commit()
# Use the relationship to get group category is in.
if (group := category.category_group) is None:
agon_logger.warning(f"{category.name}:{category.id} group relationship was None.")
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == group_id)
if (group := session.scalar(stmt)) is None:
await interaction.response.send_message("Group was not found.", ephemeral=True)
return
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f"**{category.name}** is_ascending has been updated to **{ascending}**.", ephemeral=True
)
@category.subcommand(description="Update the is_validated value for a category.")
async def is_validated(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group that Category is in.", required=True),
category_id: str = nextcord.SlashOption(
name="category", description="Category to update is_ascending of.", required=True
),
validated: bool = nextcord.SlashOption( # noqa: FBT001
description="Whether the scores need to be validated.", required=True
),
) -> None:
"""
Command to update a category's is_validated boolean.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to register channel to. Is SlashOption.
category_id (str): String representation of Category id to register channel to. Is SlashOption.
validated (bool): Boolean to set. Determines whether scores are validated or not. Is SlashOption.
"""
stmt = (
select(Category).where(col(Category.group_id) == int(group_id)).where(col(Category.id) == int(category_id))
)
with Session(engine) as session:
if (category := session.scalar(stmt)) is None:
await interaction.response.send_message("Category or Group was not found.", ephemeral=True)
return
category.is_validated = validated
session.commit()
await interaction.response.send_message(
f"**{category.name}** is_validated has been updated to **{validated}**.", ephemeral=True
)
@category.subcommand(name="name", description="Change the name of a category.")
async def category_name(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group that Category is in.", required=True),
category_id: str = nextcord.SlashOption(
name="category", description="Category to update the name of.", required=True
),
name: str = nextcord.SlashOption(description="Name to update to.", required=True, min_length=1, max_length=30),
) -> None:
"""
Command to update a category's name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to register channel to. Is SlashOption.
category_id (str): String representation of Category id to register channel to. Is SlashOption.
name (str): The name to update the category's name to. Is SlashOption.
"""
stmt = (
select(Category).where(col(Category.group_id) == int(group_id)).where(col(Category.id) == int(category_id))
)
with Session(engine) as session:
if (category := session.scalar(stmt)) is None:
await interaction.response.send_message("Category or Group was not found.", ephemeral=True)
return
old_name = category.name
category.name = name
session.commit()
# Use the relationship to get group category is in.
if (group := category.category_group) is None:
agon_logger.warning(f"{category.name}:{category.id} group relationship was None.")
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == group_id)
if (group := session.scalar(stmt)) is None:
await interaction.response.send_message("Group was not found.", ephemeral=True)
return
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f"**{old_name}** has been updated to **{category.name}**.", ephemeral=True
)
@set.subcommand(description="Commands for editing a group.")
async def group(self, interaction: nextcord.Interaction) -> None:
"""Parent slash subcommand for setting group settings. Never called."""
@group.subcommand(name="name", description="Change the name of a Group.")
async def group_name(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group that Category is in.", required=True),
name: str = nextcord.SlashOption(description="Name to update to.", required=True, min_length=1, max_length=50),
) -> None:
"""
Command to update a group's name.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to register channel to. Is SlashOption.
name (str): The name to update the group's name to. Is SlashOption.
"""
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
with Session(engine) as session:
if (group := session.scalar(stmt)) is None:
await interaction.response.send_message("Group was not found.", ephemeral=True)
return
old_name = group.name
group.name = name
# session.add(group) test if this is needed. SQLAlchemy can do the update without adding.
session.commit()
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(f"**{old_name}** has been updated to **{group.name}**.", ephemeral=True)
@group.subcommand(description="Registers the channel for a Group's messages.")
async def output_channel(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to assign to channel.", required=True),
channel: nextcord.abc.GuildChannel | None = nextcord.SlashOption(
description="Channel to register",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Command function to register a CategoryGroup's output channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to register channel to. Is SlashOption.
channel (nextcord.abc.GuildChannel, optional): Channel to register.
Is a SlashOption. Limited to text channels, Defaults to None.
"""
if interaction.guild is None or interaction.channel is None:
agon_logger.warning("Set command was called with an interaction that had no guild or channel.")
return
channel_id = interaction.channel.id if not channel else channel.id
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(col(CategoryGroup.id) == int(group_id))
)
if (group := session.scalar(stmt)) is None:
await interaction.response.send_message("Group or Category was not found.", ephemeral=True)
return
old_channel_id = group.channel_id
group.channel_id = channel_id
session.commit()
old_channel = interaction.guild.get_channel(old_channel_id)
if old_channel and isinstance(old_channel, nextcord.TextChannel | nextcord.Thread) and group.message_id:
await old_channel.get_partial_message(group.message_id).delete()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@group.subcommand(
description="Set character length for a category. Can cut off username/score. Affects all categories."
)
async def pad_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to assign pad length to.", required=True),
length: int = nextcord.SlashOption(
description="Number of characters alloted to a category.",
required=True,
default=26,
min_value=7,
max_value=MAX_LINE_LENGTH,
),
) -> None:
"""
Changes guild's category pad length.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
group_id (str): String representation of Group id to set pad length in. Is SlashOption.
length (int): Number of characters. Is SlashOption. Defaults to 26.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(col(CategoryGroup.id) == int(group_id))
)
if (group := session.scalar(stmt)) is None:
await interaction.response.send_message("Group was not found.", ephemeral=True)
return
group.pad_length = length
session.commit()
await interaction.response.send_message(
f"Category pad length set to **{length}** for **{group.name}**.", ephemeral=True
)
await send_highscore_message(session, interaction.guild, group)
@group.subcommand(
description="Registers the channel for submission messages. Currently this information is deprecated."
)
async def submission_channel(
self,
interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel | None = nextcord.SlashOption(
description="Channel to register",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Command function to register the submission message channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (nextcord.abc.GuildChannel): Channel to register
Is a SlashOption. Limited to text channels, Defaults to None.
"""
channel_id = interaction.channel.id if not channel else channel.id
with Session(engine) as session:
stmt = select(Guild).where(col(Guild.id) == interaction.guild_id)
if (guild := session.scalar(stmt)) is None:
await interaction.response.send_message("Guild was not found.", ephemeral=True)
return
guild.submission_channel = channel_id
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@group.subcommand(description="Set username length for a Group of Categories.")
async def username_length(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Group to change username length of.", required=True
),
length: int = nextcord.SlashOption(
description="Number of characters.", required=True, default=12, min_value=1, max_value=30
),
) -> None:
"""
Changes guild's username length limit.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
group_id (str): String representation of Group id to set username length in. Is SlashOption.
length (int): Number of characters. Is SlashOption. Defaults to 12.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(col(CategoryGroup.id) == int(group_id))
)
if (group := session.scalar(stmt)) is None:
await interaction.response.send_message("Group or Category was not found.", ephemeral=True)
return
group.username_length = length
session.commit()
await interaction.response.send_message(
f"Username length for **{group.name}** set to **{length}**.", ephemeral=True
)
@set.subcommand(description="Registers the channel for verification messages.")
async def verify_channel(
self,
interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel | None = nextcord.SlashOption(
description="Channel to register",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
"""
Command function to register the verification message channel.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
channel (Optional[nextcord.abc.GuildChannel]): Channel to register
Is a SlashOption. Limited to text channels, Defaults to None.
"""
channel_id = interaction.channel.id if not channel else channel.id
with Session(engine) as session:
stmt = select(Guild).where(col(Guild.id) == interaction.guild_id)
if (guild := session.scalar(stmt)) is None:
await interaction.response.send_message("Guild was not found.", ephemeral=True)
return
guild.verification_channel = channel_id
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@category_name.on_autocomplete("category_id")
@category_size.on_autocomplete("category_id")
@is_ascending.on_autocomplete("category_id")
@is_validated.on_autocomplete("category_id")
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
"""
Autocomplete function for category names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
category_id (str): String to be autocompleted with Category name.
group_id (str): Group to find Category from.
"""
await interaction.response.send_autocomplete(
autofill_categories(interaction.guild_id, int(group_id), category_id)
)
@category_name.on_autocomplete("group_id")
@category_size.on_autocomplete("group_id")
@group_name.on_autocomplete("group_id")
@is_ascending.on_autocomplete("group_id")
@is_validated.on_autocomplete("group_id")
@output_channel.on_autocomplete("group_id")
@pad_length.on_autocomplete("group_id")
@username_length.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
"""
Sends results from autofill_groups back to group_output_channel for autocompletion of group_id.
Args:
interaction (nextcord.Interaction): Interaction with command.
group_id (str): String to run autocompletion against.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
+240
View File
@@ -0,0 +1,240 @@
"""Contains command for submitting a score."""
import re
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.cogs.submission.verification_view import VerificationView
from agon.database.db import engine
from agon.database.models import Category, CategoryGroup, Guild, Submission
from agon.logger import agon_logger
from agon.utils import autofill_categories, autofill_groups, submit_score
class SubmissionCog(commands.Cog):
"""Class holds command for submitting a score."""
def __init__(self) -> None:
return
# TODO(imcompiling): fix too many return statements when reworking command.
# https://gitea.cheezers.org/imcompiling/agon/issues/76
@nextcord.slash_command()
async def submit( # noqa: PLR0911, PLR0913
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", required=True, description="Group to submit score to."),
category_id: str = nextcord.SlashOption(
name="category", required=True, description="Category to submit score to."
),
score: str = nextcord.SlashOption(required=True, description="Score to submit"),
proof: nextcord.Attachment = nextcord.SlashOption(
required=False, description="Proof for submission. Only needed if score requires verification."
),
username: str = nextcord.SlashOption(
required=False, description="Modified username if submitting under not your discord name."
),
) -> None:
"""
Submit a highscore.
Sets the username if one isn't passed in. Checks if a submission needs verified. Ensures that the score is
following the correct format. Then either creates the verification request or submits score if verification
isn't required.
Args:
interaction (nextcord.Interaction): Interaction of the command.
group_id (str): Is SlashOption. Group that the score category belongs in.
category_id (str): Is SlashOption. Category to submit the score in.
score (str): Is SlashOption. The score.
proof (nextcord.Attachment): Is SlashOption. Picture to provide proof of score.
username (str, optional): Is SlashOption. Username to associate with the score.
"""
# fill out username if not passed in
if username is None:
username = interaction.user.display_name
with Session(engine) as session:
stmt = (
select(Category)
.where(col(Category.group_id) == int(group_id))
.where(col(Category.id) == int(category_id))
)
if (category := session.scalar(stmt)) is None:
await interaction.response.send_message("Category not found.", ephemeral=True)
return
# check if verification is required. if no picture, deny
if category.is_validated and proof is None:
await interaction.response.send_message(
"This category required verification. Please attach a picture and re-submit.", ephemeral=True
)
return
# check with regex if score is in correct format
# check if verify_score was successful. It replies with corrections required itself.
if (
verified_score := await self.verify_score(interaction, score, is_time_record=category.is_time_record)
) is None:
# Verify score handles responding. Might change when submit is split up.
return
# Create submission verification request or submit score if verification not needed.
if not category.is_validated:
await submit_score(interaction, int(group_id), int(category_id), username, verified_score)
await interaction.response.send_message("Score has been submitted!", ephemeral=True)
return
stmt = select(col(Guild.verification_channel)).where(col(Guild.id) == interaction.guild_id)
if (verification_channel_id := session.scalar(stmt)) is None:
error_response = "No verification channel was registered. Contact server admins."
await interaction.response.send_message(error_response, ephemeral=True)
return
if (channel := interaction.guild.get_channel(verification_channel_id)) is None:
error_response = "Registered Verification channel does not exist. Contact server admins."
await interaction.response.send_message(error_response, ephemeral=True)
return
if not isinstance(channel, nextcord.TextChannel | nextcord.Thread):
await interaction.response.send_message("An error happened with submission.")
agon_logger.error(
"Channel id %s in guild %s is does not correspond to a TextChannel.", channel.id, channel.guild.id
)
return
# Send the verification message to admins
verification_message = await channel.send(
file=await proof.to_file(),
view=VerificationView(),
embeds=[self.create_embed(interaction, group_id, category_id, verified_score, username, session)],
content=f"Submission from <@{interaction.user.id}>.",
)
await interaction.response.send_message(content="Score submitted!", ephemeral=True)
# Save the submission data for later use.
submission: Submission = Submission(
category_id=int(category_id),
group_id=int(group_id),
guild_id=interaction.guild_id,
message_id=verification_message.id,
score=verified_score,
user_id=interaction.user.id,
username=username,
)
session.add(submission)
session.commit()
def create_embed( # noqa: PLR0913
self,
interaction: nextcord.Interaction,
group_id: str,
category_id: str,
score: str,
username: str,
session: Session,
) -> nextcord.Embed:
"""
Creates an embed containing submission data for human verification.
Args:
interaction (nextcord.Interaction): Discord interaction of the command.
group_id (str): Used to get group name.
category_id (str): Used to get category name.
score (str): Score to set.
username (str): Username to set.
session (Session): Open connection to database.
Returns:
nextcord.Embed: Embed containing human readable submission data.
"""
embed = nextcord.Embed(title="Highscores Submission", description="Review fields and accept.")
embed.add_field(name="User Name", value=username, inline=False)
# Get group name from database
stmt = (
select(col(CategoryGroup.name))
.where(col(CategoryGroup.guild_id) == interaction.guild_id)
.where(col(CategoryGroup.id) == group_id)
)
embed.add_field(name="Group", value=session.scalar(stmt), inline=False)
# Get category name from database
stmt = (
select(col(Category.name)).where(col(Category.group_id) == group_id).where(col(Category.id) == category_id)
)
embed.add_field(name="Category", value=session.scalar(stmt), inline=False)
embed.add_field(name="Score", value=score, inline=False)
return embed
async def verify_score(self, interaction: nextcord.Interaction, score: str, *, is_time_record: bool) -> str | None:
"""
Uses regex to verify that scores are in the correct format.
Handles responding to the interaction if the format is incorrect. Also standardizes the time record.
Args:
interaction (nextcord.Interaction): Discord interaction of the command.
score (str): String representation of the score being verified.
is_time_record (bool): Whether the score needs to be verified/formatted as a time record.
Returns:
str | None: String of corrected score. None if formatted incorrectly.
"""
time_regex = re.search("^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", score)
int_regex = re.search("^[0-9]+$", score)
if time_regex is None and is_time_record:
await interaction.response.send_message(
"Time record must follow format MM:SS.ms, SS.ms, or SS.", ephemeral=True
)
return None
if int_regex is None and not is_time_record:
await interaction.response.send_message("Number record must only be number.", ephemeral=True)
return None
if is_time_record:
# standardize time score format.
if re.search("^[1-9]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# score is already fine.
pass
elif re.search("^[0-9]{2}(\\.[0-9])?$", score):
# need to add leading colon and 0
score = "0:" + score
elif re.search("^[0-9](\\.[0-9])?$", score):
# need to add 0 in seconds and leading colon and 0
score = "0:0" + score
elif re.search("^[0][0-9]:[0-9]{2}(\\.[0-9])?$", score):
# if there is a minute 10ths place 0, remove
score = score[1:]
return score
@submit.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
"""
Autocomplete function for group names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String to be autocompleted with Group name.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
@submit.on_autocomplete("category_id")
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
"""
Autocomplete function for category names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
category_id (str): String to be autocompleted with Category name.
group_id (str): Group to find Category from.
"""
await interaction.response.send_autocomplete(
autofill_categories(interaction.guild_id, int(group_id), category_id)
)
+81
View File
@@ -0,0 +1,81 @@
"""Commands for updating information/variables held in database."""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Guild
from agon.utils import autofill_users_in_guild
from .views.update_username_verify import UpdateUsernameVerifyView
class UpdateCog(commands.Cog, name="Update commands."):
"""Class holds commands and views needed for updating things in bot."""
def __init__(self, bot: commands.Bot):
"""
Default constructor for Update class. Sets internal bot variable.
Args:
bot (commands.Bot): Bot object. Required to register update username button view.
"""
self._bot: commands.Bot = bot
@commands.Cog.listener()
async def on_ready(self) -> None:
"""Called when cog is loaded. Adds views required for DataManagement commands."""
self._bot.add_view(UpdateUsernameVerifyView())
@nextcord.slash_command()
async def update(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for updating things. Never called."""
@update.subcommand(description="Request a username update. Admins must approve.")
async def username(
self,
interaction: nextcord.Interaction,
old_username: str = nextcord.SlashOption(description="Username currently stored in highscores.", required=True),
new_username: str = nextcord.SlashOption(description="Username to update to.", required=True),
) -> None:
"""
Creates a request to send to admins for changing a username.
Uses UpdateUsernameVerifyView to do the work.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command
old_username (str): Username to replace.
new_username (str): Username to replace old with.
"""
with Session(engine) as session:
stmt = select(col(Guild.verification_channel)).where(col(Guild.id) == interaction.guild_id)
verify_channel = interaction.guild.get_channel(session.scalar(stmt))
if not isinstance(verify_channel, nextcord.TextChannel):
await interaction.send("Verification channel not set. Cannot process request.", ephemeral=True)
return
embed: nextcord.Embed = nextcord.Embed(title="Username change request.")
embed.add_field(name="Old Username", value=old_username)
embed.add_field(name="New Username", value=new_username)
await verify_channel.send(
content=f"Username change request from <@{interaction.user.id}>.",
embed=embed,
view=UpdateUsernameVerifyView(),
)
await interaction.send("Request sent to admins for verification.", ephemeral=True)
@username.on_autocomplete("old_username")
async def all_users_autocomplete(self, interaction: nextcord.Interaction, username: str) -> None:
"""
Sends response from autofill_users_in_guild back to update_username for autocompletion of old_username.
Args:
interaction (nextcord.Interaction): Interaction with command.
username (str): String to run autocompletion against.
"""
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
+112
View File
@@ -0,0 +1,112 @@
"""View command hierarchy."""
import nextcord
import pandas as pd
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Category, CategoryGroup, Score
from agon.utils import MAX_LINE_LENGTH, autofill_categories, autofill_groups, format_score_string
class ViewCog(commands.Cog, name="View Command"):
"""Class holds commands for viewing data."""
def __init__(self) -> None:
"""Default constructor for ViewCog class. Does nothing."""
@nextcord.slash_command()
async def view(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for view. Never called."""
@view.subcommand(description="Lists all scores for a given category.")
async def leaderboard(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Group to show leaderboard from.", required=True
),
category_id: str = nextcord.SlashOption(
name="category", description="Category to show leaderboard.", required=True
),
) -> None:
"""
Create message with leaderboard of a group and category.
Gathers all users that have a submission in the group/category combo and returns a
message with them in ranked order.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String representation of Group id to search from. Is SlashOption.
category_id (str): String representation of Category id to show leaderboard. Is SlashOption.
"""
str_list: list[str] = []
# get group, category, and all scores sorted correctly.
with Session(engine) as session:
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt)
stmt = select(Category).where(col(Category.id) == int(category_id))
category = session.scalar(stmt)
d_f = pd.read_sql_query(
sql=session.query(Score).where(col(Score.category_id) == category_id).statement, con=engine
)
scores = d_f.sort_values("Score", ascending=category.is_ascending).reset_index()
# get the longest string
longest_str: int = scores.UserName.map(len).max()
for i, row in scores.iterrows():
str_list.append(
format_score_string(
row["UserName"],
row["Score"],
longest_str,
category.is_time_record,
i + 1, # type: ignore[reportOperatorIssue]
MAX_LINE_LENGTH,
)
)
message_str: str = f"**{group.name} {category.name} Leaderboard!**\n```"
if len(str_list) == 0:
message_str += "No Submissions!"
else:
for line in str_list:
message_str += f"{line}\n"
message_str += "```"
await interaction.response.send_message(content=message_str)
@leaderboard.on_autocomplete("group_id")
async def group_autocomplete(self, interaction: nextcord.Interaction, group_id: str) -> None:
"""
Autocomplete function for group names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
group_id (str): String to be autocompleted with Group name.
"""
await interaction.response.send_autocomplete(autofill_groups(interaction.guild_id, group_id))
@leaderboard.on_autocomplete("category_id")
async def category_autocomplete(self, interaction: nextcord.Interaction, category_id: str, group_id: str) -> None:
"""
Autocomplete function for category names.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
category_id (str): String to be autocompleted with Category name.
group_id (str): Group to find Category from.
"""
await interaction.response.send_autocomplete(
autofill_categories(interaction.guild_id, int(group_id), category_id)
)
+1
View File
@@ -0,0 +1 @@
"""Package for views used in commands."""
@@ -0,0 +1,98 @@
"""Contains view and helper functions for confirming and processing deletes."""
import nextcord
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from agon.database.db import engine
from agon.database.models import Category, CategoryGroup
from agon.utils import send_highscore_message
class DeleteConfirmView(nextcord.ui.Modal):
"""View modal popup for confirming deletions."""
def __init__(self, custom_id: str):
super().__init__(title="Warning, deletion is permanent.", custom_id=custom_id)
self.checker = nextcord.ui.TextInput(
label='Type "delete" to confirm deletion.',
required=True,
placeholder="delete",
style=nextcord.TextInputStyle.short,
)
self.add_item(self.checker)
async def callback(self, interaction: nextcord.Interaction) -> None:
"""Action for confirming deletion.
Args:
interaction (nextcord.Interaction): User interaction object with the modal.
"""
if self.checker.value.lower() != "delete":
await interaction.response.send_message('Response was not "delete". Canceling delete.')
return
del_vals = parse_delete_id(self.custom_id)
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.id == del_vals["group_id"])
group = session.scalar(stmt)
if del_vals["category_id"] is None:
# delete entire group
session.delete(group)
# delete the message related to group
channel = interaction.guild.get_channel(group.channel_id)
if not isinstance(channel, nextcord.TextChannel | nextcord.Thread):
return
message: nextcord.Message = await channel.fetch_message(group.message_id)
await message.delete()
else:
stmt = (
delete(Category)
.where(Category.group_id == del_vals["group_id"])
.where(Category.id == del_vals["category_id"])
)
session.execute(stmt)
# update the message
await send_highscore_message(session, interaction.guild, group)
session.commit()
await interaction.response.send_message("Deletion complete.", ephemeral=True)
def create_delete_id(group_id: int, category_id: int | None = None) -> str:
"""
Creates a delete id string to tell the view what to delete.
Args:
group_id (int): group id to delete
category_id (Optional[int]): category id to delete. Can be None.
Returns:
str: string representation of the delete id.
"""
return f"delete-{group_id}-{category_id}"
def parse_delete_id(delete_id: str) -> dict:
"""
Returns a dict containing the values of a delete id.
Args:
delete_id (str): the delete id
Returns:
dict: dict containing values from a delete id.
"""
vals = {}
group_id = delete_id.split("-")[1]
category_id = delete_id.split("-")[2]
vals["group_id"] = group_id
if category_id == "None":
vals["category_id"] = None
else:
vals["category_id"] = category_id
return vals
@@ -0,0 +1,71 @@
"""View for allowing a username update."""
import nextcord
from sqlalchemy import distinct, exc, select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Category, CategoryGroup, Score
from agon.utils import send_highscore_message
class UpdateUsernameVerifyView(nextcord.ui.View):
"""Defines view for allowing a username update."""
def __init__(self) -> None:
"""Default constructor for UpdateUsernameVerifyView. Calls super constructor with no timeout."""
super().__init__(timeout=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-username-update")
async def deny_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Button for denying username update. Deletes message.
Args:
button (nextcord.ui.Button): unused
interaction (nextcord.Interaction): Interaction with the button
"""
await interaction.message.delete()
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-username-update")
async def approve_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Button for approving username update.
Tells guild data to update username, then updates message to show it is complete.
Args:
button (nextcord.ui.Button): unused
interaction (nextcord.Interaction): Interaction with the button
"""
with Session(engine) as session:
try:
embed_content = interaction.message.embeds[0].to_dict()
old_username: str = embed_content["fields"][0]["value"] # type: ignore reportTypedDictNotRequiredAccess
new_username: str = embed_content["fields"][1]["value"] # type: ignore reportTypedDictNotRequiredAccess
stmt = select(distinct(col(Score.category_id))).where(col(Score.username) == old_username)
category_ids: list[int] = list(session.scalars(stmt))
stmt = select(Score).where(col(Score.username) == old_username)
for score in session.scalars(stmt):
score.UserName = new_username
session.commit()
group_ids = select(col(Category.group_id)).where(col(Category.id).in_(category_ids))
stmt = select(CategoryGroup).where(col(CategoryGroup.id).in_(group_ids))
for group in session.scalars(stmt):
await send_highscore_message(session, interaction.guild, group)
content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>."
await interaction.message.edit(content=content, embeds=interaction.message.embeds, view=None)
except exc.DataError:
session.rollback()
content = interaction.message.content
content += "\nError, UserName is too long."
await interaction.message.edit(content=content, embeds=interaction.message.embeds, view=None)
await interaction.response.send_message(content="User must resubmit with shorter name.", ephemeral=True)
+69
View File
@@ -0,0 +1,69 @@
"""Defines cog for guild listeners. Contains functions for various guild events."""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Guild
class GuildListener(commands.Cog, name="Guild Listener"):
"""Sets up message listener."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild) -> None:
"""
Listener for Guild Join. Adds guild to database.
Args:
guild (nextcord.Guild): Guild that was joined.
"""
with Session(engine) as session:
stmt = select(Guild).where(col(Guild.id == guild.id))
# just in case check if the guild already exists
if (guild_data := session.scalar(stmt)) is None:
new_guild = Guild(id=guild.id, name=guild.name)
session.add(new_guild)
else:
guild_data.GuildName = guild.name
guild_data.Active = True
session.commit()
# let imcompiling#2272 know about a new server joining!
# staying with fetch due to intents not being set up correctly. Maybe change to get eventually.
imcompiling: nextcord.User = await self.bot.fetch_user(206602713511886849)
await imcompiling.send(f'Agon has joined server "{guild.name}"!')
@commands.Cog.listener()
async def on_guild_remove(self, guild: nextcord.Guild) -> None:
"""
Listens to when the bot has been removed from a guild. Update's the database to record the active flag as false.
Args:
guild (nextcord.Guild): Guild that was left.
"""
with Session(engine) as session:
stmt = select(Guild).where(col(Guild.id == guild.id))
guild_data = session.scalar(stmt)
guild_data.Active = False
session.commit()
# let imcompiling#2272 know about leaving a server.
# staying with fetch due to intents not being set up correctly. Maybe change to get eventually.
imcompiling: nextcord.User = await self.bot.fetch_user(206602713511886849)
await imcompiling.send(f'Agon has been removed from "{guild.name}".')
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot) -> None:
"""Adds function to bot."""
bot.add_cog(GuildListener(bot))
+14
View File
@@ -0,0 +1,14 @@
"""Initializing code for all loops."""
from nextcord.ext import commands
from .bot_presence import BotPresence
from .guild_data import GuildData
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot) -> None:
"""Adds function to bot."""
bot.add_cog(BotPresence(bot))
bot.add_cog(GuildData(bot))
+45
View File
@@ -0,0 +1,45 @@
"""Functions for controlling the bot's presence. Also contains the loop."""
import random
import nextcord
from nextcord.ext import commands, tasks
class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions."""
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild) -> None: # noqa: ARG002
"""Listener for Guild Join. Updated presence on guild join."""
await self.change_presence()
@commands.Cog.listener()
async def on_ready(self) -> None:
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
self.periodic_change_presence.start()
@tasks.loop(minutes=2)
async def periodic_change_presence(self) -> None:
"""Loops every 2 minutes and changes presence."""
await self.change_presence()
async def change_presence(self) -> None:
"""Randomly chooses a presence and updates bot's presence."""
statuses = [
"Now with slash commands!",
"Vuln Pls.",
"All your base are belong to us.",
"Double plus good.",
"That's a lot of damage!",
]
status = random.choice(statuses) # noqa: S311
status += f" | {len(self.bot.guilds)} servers"
await self.bot.change_presence(activity=nextcord.Game(status))
+44
View File
@@ -0,0 +1,44 @@
"""Contains a loop that updates guild data in the database periodically."""
from nextcord.ext import commands, tasks
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Guild
class GuildData(commands.Cog, name="Guild Data"):
"""Class to contain loop."""
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_ready(self) -> None:
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
await self.periodic_update_guild_data.start()
@tasks.loop(hours=24)
async def periodic_update_guild_data(self) -> None:
"""Loops every day, calls update_guild_data."""
self.update_guild_data()
def update_guild_data(self) -> None:
"""Updates all known guild's data in the database."""
with Session(engine) as session:
for guild in self.bot.guilds:
# update given guild name for guild id
stmt = select(Guild).where(col(Guild.id) == guild.id)
guild_data = session.scalar(stmt)
# just in case checking if the guild doesn't exist.
if guild_data is None:
new_guild = Guild(id=guild.id, name=guild.name)
session.add(new_guild)
else:
guild_data.name = guild.name
session.commit()
@@ -1,6 +1,4 @@
"""
Cog for creating submission process.
"""
"""Cog for creating submission process."""
from nextcord.ext import commands
@@ -15,14 +13,14 @@ class SubmissionCog(commands.Cog, name="Submission Cog"):
self.bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""Called when cog is loaded"""
async def on_ready(self) -> None:
"""Called when cog is loaded."""
self.bot.add_view(SubmissionCreateButton(self.bot))
self.bot.add_view(VerificationView())
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
def setup(bot: commands.Bot) -> None:
"""Adds function to bot."""
bot.add_cog(SubmissionCog(bot))
+23
View File
@@ -0,0 +1,23 @@
"""Views used for submission process."""
import nextcord
from nextcord.ext import commands
class SubmissionCreateButton(nextcord.ui.View):
"""Defines view for score submission button."""
def __init__(self, bot: commands.Bot):
super().__init__(timeout=None)
self._bot = bot
@nextcord.ui.button(label="Submit", style=nextcord.ButtonStyle.primary, custom_id="submitcreate")
async def submitcreate_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Button for score submission.
Is deprecated, sends response telling user to use the new command instead.
"""
await interaction.response.send_message(
"This feature has been deprecated. Please use the '/submit' slash command instead.", ephemeral=True
)
+54
View File
@@ -0,0 +1,54 @@
"""View for verification buttons."""
import nextcord
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Submission
from agon.utils import submit_score
class VerificationView(nextcord.ui.View):
"""Defines views for verifying score submissions."""
def __init__(self) -> None:
super().__init__(timeout=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-button-submission")
async def deny_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""Button for denying score submission."""
with Session(engine) as session:
stmt = (
delete(Submission)
.where(col(Submission.guild_id) == interaction.guild_id)
.where(col(Submission.message_id) == interaction.message.id)
)
session.execute(stmt)
session.commit()
await interaction.message.delete()
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission")
async def approve_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""Button for approving score submission."""
with Session(engine) as session:
stmt = (
select(Submission)
.where(col(Submission.guild_id) == interaction.guild_id)
.where(col(Submission.message_id) == interaction.message.id)
)
if (submission := session.scalar(stmt)) is None:
await interaction.response.send_message("Could not find submission data in database.", ephemeral=True)
await interaction.message.delete()
return
await submit_score(
interaction, submission.group_id, submission.category_id, submission.username, submission.score
)
session.delete(submission)
session.commit()
content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>."
await interaction.message.edit(content=content, view=None)
+16
View File
@@ -0,0 +1,16 @@
"""Loads environment variables for use in bot."""
import os
from dotenv.main import load_dotenv
load_dotenv()
# Discord config
BOT_PREFIX = "?"
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
SQL_DB = os.getenv("SQL_DB", "")
SQL_HOST = os.getenv("SQL_HOST", "")
SQL_PASS = os.getenv("SQL_PASS", "")
SQL_USER = os.getenv("SQL_USER", "")
ENVIRONMENT: str = os.getenv("ENVIRONMENT", "prod")
+1
View File
@@ -0,0 +1 @@
"""Package for database interaction."""
+19
View File
@@ -0,0 +1,19 @@
"""Simple file to set up the sqlmodel connection to database."""
from sqlmodel import SQLModel, create_engine
from agon import config
from agon.logger import agon_logger
from . import models # noqa: F401 - Need this import for creating the models.
if config.ENVIRONMENT == "prod":
url = f"mysql+pymysql://{config.SQL_USER}:{config.SQL_PASS}@{config.SQL_HOST}/{config.SQL_DB}"
agon_logger.info("Connected to prod database.")
else:
url = "sqlite:///database/database.db"
agon_logger.info("Connected to local dev database.")
engine = create_engine(url)
# Don't need to create all when connected to prod.
SQLModel.metadata.create_all(engine)
+186
View File
@@ -0,0 +1,186 @@
"""File holds all model definitions of SQL tables."""
from sqlalchemy.dialects.mysql import BIGINT, BOOLEAN, FLOAT, INTEGER, SMALLINT, VARCHAR
from sqlmodel import Column, Field, ForeignKey, Relationship, SQLModel, UniqueConstraint
class Guild(SQLModel, table=True):
"""
Represents a Guild entity in the database.
Attributes:
id (int): The unique identifier for the guild.
name (str): The name of the guild.
submission_channel (int): The ID of the submission channel associated with the guild.
verification_channel (int): The ID of the verification channel associated with the guild.
active (bool): Indicates whether the guild is active. Defaults to True.
category_groups (list[CategoryGroup]): A list of category groups associated with the guild.
"""
__tablename__: str = "Guilds"
id: int = Field(sa_column=Column("GuildId", BIGINT(unsigned=True), primary_key=True, nullable=False))
name: str = Field(sa_column=Column("GuildName", VARCHAR), max_length=95)
# deprecated, not currently used.
submission_channel: int | None = Field(
sa_column=Column("SubmissionChannel", BIGINT(unsigned=True), nullable=True), default=None
)
verification_channel: int | None = Field(
sa_column=Column("VerificationChannel", BIGINT(unsigned=True), nullable=True), default=None
)
active: bool = Field(sa_column=Column("Active", BOOLEAN), default=True)
category_groups: list["CategoryGroup"] = Relationship(back_populates="guild", cascade_delete=True)
class CategoryGroup(SQLModel, table=True):
"""
Represents a category group in the database.
Attributes:
id (int | None): The primary key of the category group.
name (str): The name of the category group, with a maximum length of 50 characters.
channel_id (int): The ID of the channel associated with the category group.
guild_id (int): The ID of the guild associated with the category group.
message_id (int): The ID of the message associated with the category group.
pad_length (int): The padding length for the category group, default is 12.
username_length (int): The username length for the category group, default is 12.
guild (Guild | None): The guild relationship back-populated from the Guild model.
Table Constraints:
__table_args__: Unique constraint on guild_id and name to ensure unique category groups within a guild.
"""
__tablename__: str = "CategoryGroups"
id: int | None = Field(sa_column=Column("GroupId", INTEGER, primary_key=True), default=None)
name: str = Field(sa_column=Column("GroupName", VARCHAR), max_length=50)
channel_id: int = Field(sa_column=Column("ChannelId", BIGINT(unsigned=True)))
guild_id: int = Field(
sa_column=Column(
"GuildId",
BIGINT(unsigned=True),
ForeignKey("Guilds.GuildId", ondelete="CASCADE"),
nullable=False,
)
)
message_id: int | None = Field(sa_column=Column("MessageId", BIGINT(unsigned=True)), default=None)
pad_length: int = Field(default=12, sa_column=Column("PadLength", SMALLINT(unsigned=True)))
username_length: int = Field(default=12, sa_column=Column("UsernameLength", SMALLINT(unsigned=True)))
__table_args__ = (UniqueConstraint("GuildId", "GroupName", name="UniqueGuildCategoryGroup"),)
guild: Guild | None = Relationship(back_populates="category_groups")
categories: list["Category"] | None = Relationship(back_populates="category_group", cascade_delete=True)
class Category(SQLModel, table=True):
"""
Represents a category in the database.
Attributes:
id (int): The unique identifier for the category. Primary key.
name (str): The name of the category with a maximum length of 30 characters.
group_id (int): The identifier of the group to which the category belongs.
Foreign key referencing CategoryGroups.id.
size (int): The size attribute of the category, default is 3.
is_ascending (bool): Indicates if the category is in ascending order. Default is True.
is_time_record (bool): Indicates if the category is a time record. Default is False.
is_validated (bool): Indicates if the category is validated. Default is True.
category_group (CategoryGroup | None): The relationship to the CategoryGroup model.
scores (list["Score"] | None): The relationship to the Score model with cascade delete enabled.
Table Constraints:
__table_args__: Unique constraint on group_id and name to ensure unique category names within a group.
"""
__tablename__: str = "Categories"
id: int = Field(sa_column=Column("CategoryId", INTEGER, primary_key=True), default=None)
name: str = Field(sa_column=Column("CategoryName", VARCHAR), max_length=30)
group_id: int = Field(
sa_column=Column(
"GroupId",
INTEGER(unsigned=True),
ForeignKey("CategoryGroups.GroupId", ondelete="CASCADE"),
nullable=False,
)
)
size: int = Field(sa_column=Column("Size", SMALLINT(unsigned=True), nullable=False), default=3)
is_ascending: bool = Field(sa_column=Column("IsAscending", BOOLEAN, nullable=False), default=True)
is_time_record: bool = Field(sa_column=Column("IsTimeRecord", BOOLEAN, nullable=False), default=False)
is_validated: bool = Field(sa_column=Column("IsValidated", BOOLEAN, nullable=False), default=True)
__table_args__ = (UniqueConstraint("GroupId", "CategoryName", name="UniqueCategoryInGroup"),)
category_group: CategoryGroup | None = Relationship(back_populates="categories")
scores: list["Score"] | None = Relationship(back_populates="category", cascade_delete=True)
class Score(SQLModel, table=True):
"""
Represents a score entry in the database.
Attributes:
username (str): The username associated with the score. This is a primary key.
score (float): The score value.
category_id (int): The ID of the category this score belongs to. This is a primary key and a foreign key
referencing the Categories table.
category (Category | None): The relationship to the Category model, back-populated by the scores attribute
in the Category model.
"""
__tablename__: str = "Scores"
username: str = Field(sa_column=Column("UserName", VARCHAR, primary_key=True, nullable=False), max_length=30)
score: float = Field(sa_column=Column("Score", FLOAT, nullable=False))
category_id: int = Field(
sa_column=Column(
"CategoryId",
INTEGER(unsigned=True),
ForeignKey("Categories.CategoryId", ondelete="CASCADE"),
nullable=False,
primary_key=True,
),
)
category: Category | None = Relationship(back_populates="scores")
class Submission(SQLModel, table=True):
"""
Represents a submission in the database.
Attributes:
category_id (int): The ID of the category. This is a primary key.
group_id (int): The ID of the group.
guild_id (int): The ID of the guild.
message_id (int): The ID of the message.
score (str): The score of the submission, with a maximum length of 30 characters.
user_id (int): The ID of the user.
username (str): The username of the user. This is a primary key.
"""
__tablename__: str = "Submissions"
category_id: int = Field(
sa_column=Column(
"CategoryId",
INTEGER(unsigned=True),
nullable=False,
primary_key=True,
),
)
group_id: int = Field(
sa_column=Column(
"GroupId",
INTEGER(unsigned=True),
nullable=False,
)
)
guild_id: int = Field(sa_column=Column("GuildId", BIGINT(unsigned=True), nullable=False))
message_id: int = Field(sa_column=Column("MessageId", BIGINT(unsigned=True), nullable=False))
# Score is stored as a string until converted during submission.
score: str = Field(sa_column=Column("Score", VARCHAR, nullable=False), max_length=30)
user_id: int = Field(sa_column=Column("UserId", BIGINT(unsigned=True), nullable=False))
username: str = Field(sa_column=Column("UserName", VARCHAR, nullable=False, primary_key=True), max_length=30)
+10
View File
@@ -0,0 +1,10 @@
"""Creates logger for Agon bot."""
import logging
import sys
agon_logger = logging.getLogger("default")
agon_logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
agon_logger.addHandler(handler)
+416
View File
@@ -0,0 +1,416 @@
"""Contains various utility functions for highscores bot."""
import time
import nextcord
import pandas as pd
from sqlalchemy import distinct, select
from sqlalchemy.orm import Session, aliased
from sqlmodel import col
from agon.database.db import engine
from agon.database.models import Category, CategoryGroup, Guild, Score
from agon.logger import agon_logger
MAX_LINE_LENGTH = 142
def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "dict[str, str]":
"""
Returns all category names that start with category_begin.
If category_begin is empty, returns list of all category names.
Args:
guild_id (int): Guild id that holds the group and categories.
group_id (int): Group id to search in.
category_begin (str): The beginning of a category name to search with. Can be empty.
Returns:
dict[str, str]: List of all category names that start with category_begin.
"""
with Session(engine) as session:
# search database for all category names
subq = (
select(CategoryGroup)
.where(col(CategoryGroup.guild_id) == guild_id)
.where(col(CategoryGroup.id) == group_id)
.subquery()
)
cat_group_alias = aliased(CategoryGroup, subq)
stmt = select(Category).where(col(Category.group_id) == cat_group_alias.id)
categories_dict: dict[str, str] = {cat.name: str(cat.id) for cat in session.scalars(stmt)}
# if string empty, return all category names
if not category_begin:
return dict(list(categories_dict.items())[:25])
# else find all categories that start with category_begin
categories: dict[str, str] = {
cat_name: cat_id for cat_name, cat_id in categories_dict.items() if category_begin.lower() in cat_name.lower()
}
return dict(list(categories.items())[:25])
def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
"""
Returns all group names that start with group_begin. If group_begin is empty, returns list of all group names.
Args:
guild_id (int): Guild id that holds the groups.
group_begin (str): The beginning of a group name to search with. Can be empty.
Returns:
dict[str, str]: Dict of group names and ids where group name start with group_begin. Key is name, value is id.
"""
with Session(engine) as session:
# Select all groups in guild.
stmt = select(CategoryGroup).where(col(CategoryGroup.guild_id) == guild_id)
# dict has to have key values of same type for discord.
group_dict: dict[str, str] = {group.name: str(group.id) for group in session.scalars(stmt)}
# if string empty, return all group names
if not group_begin:
return dict(list(group_dict.items())[:25])
# else find all groups that start with group_begin
groups: dict[str, str] = {
group_name: group_id for group_name, group_id in group_dict.items() if group_begin.lower() in group_name.lower()
}
return dict(list(groups.items())[:25])
def autofill_users_in_group_category(guild_id: int, group_id: int, category_id: int, user_begin: str) -> "list[str]":
"""
Returns list of users that match user_begin.
Either all users in group or just users in category if category is not None. Might be expensive due to union-ing all
users in group.
Args:
guild_id (int): Guild id corresponding to group and category.
group_id (str): group to look in for category.
category_id (str): Category to look in for users. Can be None if not looking for category.
user_begin (str): Beginning of user name to search with. Can be empty.
Returns:
list[str]: List of users that start with user_begin.
"""
with Session(engine) as session:
# get all categories in the group
subq = (
select(CategoryGroup)
.where(col(CategoryGroup.guild_id) == guild_id)
.where(col(CategoryGroup.id) == group_id)
.subquery()
)
category_group = aliased(CategoryGroup, subq)
# If no category id, get all users in group.
if category_id == 0:
subq = select(col(Category.id)).where(col(Category.group_id) == category_group.id)
stmt = select(distinct(col(Score.username))).where(col(Score.category_id).in_(subq))
else:
# get all users in category_id
subq = (
select(Category)
.where(col(Category.group_id) == category_group.id)
.where(col(Category.id) == category_id)
.subquery()
)
category = aliased(Category, subq)
stmt = select(distinct(col(Score.username))).where(col(Score.category_id) == category.id)
users_list: list[str] = list(session.scalars(stmt))
# if string empty, return all users
if not user_begin:
return users_list[:25]
# else find all users that start with user_begin
users: list[str] = [user for user in users_list if user_begin.lower() in user.lower()]
return users[:25]
def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
"""
Obtains and returns a list of all the users who have submitted to the guild's highscores.
Args:
guild_id (int): Id of the guild.
user_begin (str): Beginning of user name to search with. Can be empty.
Returns:
list[str]: All users with submissions in the guild.
"""
with Session(engine) as session:
group_ids = select(col(CategoryGroup.id)).where(col(CategoryGroup.guild_id) == guild_id)
category_ids = select(col(Category.id)).where(col(Category.group_id).in_(group_ids))
stmt = select(distinct(col(Score.username))).where(col(Score.category_id).in_(category_ids))
users_list: list[str] = list(session.scalars(stmt))
# if string empty, return all users
if not user_begin:
return users_list[:25]
# else find all users that start with user_begin
users: list[str] = [user for user in users_list if user_begin.lower() in user.lower()]
return users[:25]
# TODO(imcompiling): #73 Address complexity warning.
# 073
def format_highscore_message(session: Session, group: CategoryGroup) -> str:
"""
Creates a formatted string for a category's highscore discord message.
This formatted string is in discord's code block to ensure monospace text.
Loops through each category to build the message one place at a time.
Args:
session (Session): the sqlalchemy session used for this function call.
group (CategoryGroup): group object that contains the contents of the message.
Returns:
str: Formatted string for discord message
"""
ret_string = f"**{group.name}**\n```"
stmt = select(Guild).where(col(Guild.id) == group.guild_id)
# Get all categories in the group
subq = (
select(CategoryGroup)
.where(col(CategoryGroup.guild_id) == group.guild_id)
.where(col(CategoryGroup.name) == group.name)
.subquery()
)
cat_group_alias = aliased(CategoryGroup, subq)
stmt = select(Category).where(col(Category.group_id) == cat_group_alias.id)
# set up a map for the categories
# also grab the max category size
categories: list[Category] = []
category_ids = []
max_cat_size: int = 0
pad_length = group.pad_length
for cat in session.scalars(stmt):
max_cat_size = max(cat.size, max_cat_size)
categories.append(cat)
category_ids.append(cat.id)
# get all scores in category group, load into data frame.
d_f = pd.read_sql_query(
sql=session.query(Score).where(col(Score.category_id).in_(category_ids)).statement,
con=engine,
)
# this list is what will be printed in the discord message
# first string will be category list, others will be score printouts
str_list: list[str] = ["" for i in range(max_cat_size + 1)]
groups_df = d_f.groupby("CategoryId")
for category in categories:
# category name string
str_list[0] += f"{category.name}".ljust(pad_length, " ")
# ensure there is data to print
if category.id in groups_df.indices:
group_df = groups_df.get_group(category.id)
# process data. Want each group sorted correctly and only top X results
processed_group = (
group_df.sort_values("Score", ascending=category.is_ascending).head(category.size).reset_index()
)
# add each score to list
for ind in range(max_cat_size): # need to subtract to deal with category name string
if ind in processed_group.index:
username = processed_group["UserName"][ind]
score = processed_group["Score"][ind]
str_list[ind + 1] += format_score_string(
username,
score,
group.username_length,
category.is_time_record,
ind + 1,
pad_length,
)
# if not enough entries, pad out
elif ind + 1 <= category.size:
str_list[ind + 1] += f"{ind + 1}: ".ljust(pad_length, " ")
else:
str_list[ind + 1] += "".ljust(pad_length, " ")
else:
for ind in range(max_cat_size):
if ind + 1 <= category.size:
str_list[ind + 1] += f"{ind + 1}: ".ljust(pad_length, " ")
else:
str_list[ind + 1] += "".ljust(pad_length, " ")
# next step is to cut down on the length of each line if required
# need to use index to allow for modifying the list
for ind, line in enumerate(str_list):
if len(line) > MAX_LINE_LENGTH:
split_loc = int(MAX_LINE_LENGTH / pad_length) * pad_length
new_line = line[split_loc:]
str_list[ind] = line[:split_loc]
str_list.append(new_line)
# this list can now be placed into the format message function in the bot
for ind, string in enumerate(str_list):
# rstrip to clear unneeded spaces
# add newline here for each string
ret_string += string.rstrip() + "\n"
# add extra newline at category split unless there is already a space.
if ((ind + 1) % (max_cat_size + 1)) == 0 and not ret_string.endswith("\n\n"):
ret_string += "\n"
return ret_string + "```"
# TODO(imcompiling): rework into two functions to remove the boolean and reduce function arguments.
# 084
def format_score_string(
username: str,
score: float,
username_length: int,
is_time_record: bool,
rank: int,
pad_length: int,
) -> str:
"""
Builds the string to show a user's score.
Args:
username (str): username.
score (int): score in int form.
username_length (float): limit to how long username is allowed to be.
is_time_record (bool): if the score represents an number or a time record.
rank (int): the user's rank.
pad_length (int): limit to how long the string can be.
Returns:
str: string to show user's score.
"""
username = username[:username_length].ljust(username_length, " ")
# get score, pad to correct length
if is_time_record:
# MM:SS.ms is 7 characters long
# if there are single digit minutes, add a space for formatting.
formatted_score = get_time_str(score)
if formatted_score.find(":") and len(formatted_score.split(":", maxsplit=1)[0]) < 2: # noqa: PLR2004
formatted_score = " " + formatted_score
else:
formatted_score = str(int(score)).rjust(7, " ")
return f"{rank}: {username} - {formatted_score}".ljust(pad_length, " ")
def get_time_str(time_in: float) -> str:
"""
Formats a float value into the desired string format for a timestamp.
Args:
time_in (float): A time record in seconds
Returns:
str: Describes the time record in string format.
"""
# TODO see if theres a better way to do this
milliseconds = round((time_in - int(time_in)) * 10)
return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(time_in))
async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> None:
"""
Function takes care of checking if a message exists for a group.
Creates and sends/edits the message.
Args:
session (Session): The sqlalchemy session being used for this function call.
guild (nextcord.Guild): the nextcord.Guild object requesting this message.
group (CategoryGroup): group to post to the message.
"""
highscore_string = format_highscore_message(session, group)
if (channel := guild.get_channel(group.channel_id)) is None:
agon_logger.error("Channel id %s in guild %s does not exist.", group.channel_id, guild.id)
return
if not isinstance(channel, nextcord.TextChannel | nextcord.Thread):
agon_logger.error(
"Channel id %s in guild %s is does not correspond to a TextChannel or Thread.", channel.id, channel.guild.id
)
return
if group.message_id is None:
# message doesn't exist.
message: nextcord.Message = await channel.send(highscore_string)
group.message_id = message.id
session.commit()
else:
try:
message: nextcord.Message = await channel.fetch_message(group.message_id)
await message.edit(content=highscore_string)
except nextcord.NotFound:
# Message was deleted on discord's side.
message: nextcord.Message = await channel.send(highscore_string)
group.message_id = message.id
session.commit()
return
async def submit_score(
interaction: nextcord.Interaction,
group_id: int,
category_id: int,
username: str,
score: str,
) -> None:
"""
Adds passed score to group. Then updates group message by calling send_highscore_message.
Ensures that a highscores channel exists.
Args:
interaction (nextcord.Interaction): Interaction that caused the score submission.
group_id (int): group id to submit score to.
category_id (int): Category id to submit score to.
username (str): The user who submitted the score.
score (str): Score to submit. Need to convert to correct type.
"""
with Session(engine) as session:
stmt = select(col(Category.is_time_record)).where(col(Category.id) == category_id)
is_time_record = session.scalar(stmt)
if is_time_record:
minutes = float(score.split(":", maxsplit=1)[0])
seconds = float(score.split(":", maxsplit=1)[1])
score_f = (minutes * 60) + seconds
else:
score_f = float(score)
stmt = select(Score).where(col(Score.username) == username).where(col(Score.category_id) == category_id)
existing_score = session.scalar(stmt)
if existing_score:
existing_score.score = score_f
else:
score_obj = Score(username=username, score=score_f, category_id=category_id)
session.add(score_obj)
session.commit()
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == group_id)
group = session.scalar(stmt)
await send_highscore_message(session, interaction.guild, group)
-190
View File
@@ -1,190 +0,0 @@
"""File for Category class."""
import json
from classes.score import Score, ScoreTime # pylint: disable=import-error
class Category:
"""Stores a category's information and relative scores."""
def __init__(self, name: str) -> None:
"""
Default constructor for Category class. Creates empty category and populates with defaults.
Args:
name (str): Category's name
"""
self._name: str = name
self._ascending: bool = False
self._time_record: bool = False
self._scores_map: dict = {}
def __str__(self) -> str:
"""
Returns Category class as a string for printing.
Returns:
str: Category class as a string
"""
return self.to_str()
@classmethod
def from_dict(cls, name: str, data: dict):
"""
Creates a Category class from the passed data.
Args:
name (str): Category name
data (dict): Rest of Category data in dictionary format
Returns:
Category: Populated Category object
"""
category = cls(name)
category.ascending = data["ascending"]
category.time_record = data["time_record"]
for user, score in data["scores"].items():
if category.time_record:
score_obj: ScoreTime = ScoreTime(user)
score_obj.value = float(score)
category.add_score(score_obj)
else:
score_obj: Score = Score(user)
score_obj.value = int(score)
category.add_score(score_obj)
return category
@property
def ascending(self) -> bool:
"""
Getter for is ascending.
Returns:
bool: is ascending
"""
return self._ascending
@ascending.setter
def ascending(self, ascending: bool) -> None:
"""
Setter for is ascending.
Args:
ascending (bool): is ascending
"""
self._ascending = ascending
@property
def name(self) -> str:
"""
Getter for name.
Returns:
str: name
"""
return self._name
@property
def time_record(self) -> bool:
"""
Getter for is time record.
Returns:
bool: is time record
"""
return self._time_record
@time_record.setter
def time_record(self, time_record: bool) -> None:
"""
Setter for is time record.
Args:
time_record (bool): is time record
"""
self._time_record = time_record
@property
def scores(self) -> "list[Score]":
"""
Getter for Scores list.
Returns:
list[Score]: List of Scores in this Category
"""
return self._scores_map.values()
def add_score(self, score: Score) -> None:
"""
Adds score to dictionary storage map. Sorts dictionary by score value.
Args:
score (Score): Score to be added
"""
if score.user in self._scores_map:
self._scores_map[score.user].value = score.value
else:
self._scores_map[score.user] = score
self._scores_map = dict(
sorted(self._scores_map.items(), key=lambda x: x[1].value, reverse=not self.ascending)
)
def clear(self) -> None:
"""
Clears all Scores tracked in score map.
This is a test function and shouldn't really be used.
"""
self._scores_map.clear()
def get_user_score(self, user: str) -> Score:
"""
Gets a user's score. Returns None if user is not found.
Args:
user (str): Name of score to find
Returns:
Score: User's Score or None
"""
if user in self._scores_map:
return self._scores_map[user]
return None
def remove_score(self, score: str) -> None:
"""
Removes a user's score from dictionary storage.
Args:
score (str): user whose score to remove
"""
score = self._scores_map.pop(score)
del score
def to_dict(self) -> dict:
"""
Saves category data as a python dictionary.
Does not include category name. It should be the key of above data structure.
Returns:
dict: Category class in dictionary format
"""
data = {}
data["ascending"] = self.ascending
data["time_record"] = self.time_record
data["scores"] = {}
for score in self.scores:
data["scores"][score.user] = score.value
return data
def to_str(self) -> str:
"""
Returns Category object as a string.
Returns:
str: string of json representation of Category
"""
return json.dumps(self.to_dict(), indent=4)
-211
View File
@@ -1,211 +0,0 @@
"""File for Guild class."""
import json
from classes.table import Table # pylint: disable=import-error
class Guild:
"""Stores a guild's information."""
def __init__(self, guild_id: int) -> None:
"""
Default constructor for Guild class. Initializes channel ids as 0 as they won't be
registered yet. Sets default username length of 12. Default highscores size to 3.
Args:
guild_id (int): Discord Guild id of new Guild
"""
self._guild_id: int = guild_id
self._highscore_channel_id: int = 0
self._submission_channel_id: int = 0
self._table_map: dict = {}
self._username_length: int = 12
self._verification_channel_id: int = 0
def __str__(self) -> str:
"""
Returns Guild class as a string for printing.
Returns:
str: Guild class as a string
"""
return self.to_str()
@classmethod
def from_dict(cls, guild_id: int, data: dict):
"""
Creates a Guild class from the passed data.
Args:
guild_id (int): Guild's id
data (dict): Rest of Guild data in dictionary format
Returns:
Guild: Populated Guild object
"""
guild = cls(guild_id)
guild.highscore_channel = data["highscore_channel_id"]
guild.submission_channel = data["submission_channel_id"]
guild.username_length = data["username_length"]
guild.verification_channel = data["verification_channel_id"]
for table_name, table_data in data["tables"].items():
table = Table.from_dict(table_name, table_data)
guild.add_table(table)
return guild
@property
def guild_id(self) -> int:
"""
Getter for Guild id
Returns:
int: Guild id
"""
return self._guild_id
@property
def highscore_channel(self) -> int:
"""
Getter for highscore channel id.
Returns:
int: highscore channel id
"""
return self._highscore_channel_id
@highscore_channel.setter
def highscore_channel(self, channel: int) -> None:
"""
Setter for highscore channel id.
Args:
channel (int): highscore channel id
"""
self._highscore_channel_id = channel
@property
def submission_channel(self) -> int:
"""
Getter for submission channel id.
Returns:
int: submission channel id
"""
return self._submission_channel_id
@submission_channel.setter
def submission_channel(self, channel: id) -> None:
"""
Setter fro submission channel id.
Args:
channel (id): submission channel id
"""
self._submission_channel_id = channel
@property
def tables(self) -> "list[Table]":
"""
Getter for Tables list.
Returns:
list[Table]: List of Tables in this Guild
"""
return self._table_map.values()
@property
def username_length(self) -> int:
"""
Getter for username length.
Returns:
int: username length
"""
return self._username_length
@username_length.setter
def username_length(self, length: int) -> None:
"""
Setter for username length.
Args:
length (int): username length
"""
self._username_length = length
@property
def verification_channel(self) -> int:
"""
Getter for verification channel id.
Returns:
int: verification channel id
"""
return self._verification_channel_id
@verification_channel.setter
def verification_channel(self, channel: int) -> None:
"""
Setter for verification channel id
Args:
channel (int): verification channel id
"""
self._verification_channel_id = channel
def add_table(self, table: Table) -> None:
"""
Adds a Table to table map. Shouldn't exist already.
Args:
table (Table): new Table to add
"""
if table.name not in self._table_map:
self._table_map[table.name] = table
def get_table(self, table_name: str) -> Table:
"""
Getter for specific Table. Returns None if Table is not found.
Args:
table_name (str): Name of Table to find
Returns:
Table: Table corresponding to Name. None if not found.
"""
if table_name in self._table_map:
return self._table_map[table_name]
return None
def to_dict(self) -> dict:
"""
Creates python dictionary object from class data.
Does not include guild_id in dict, it should be the key of above data structure.
Returns:
dict: Guild class in dictionary format
"""
data: dict = {}
data["highscore_channel_id"] = self.highscore_channel
data["submission_channel_id"] = self.submission_channel
data["tables"] = {}
data["username_length"] = self.username_length
data["verification_channel_id"] = self.verification_channel
for table in self.tables:
data["tables"][table.name] = table.to_dict()
return data
def to_str(self) -> str:
"""
Returns Guild object as a string.
Returns:
str: string of json representation of Guild
"""
return json.dumps(self.to_dict(), indent=4)
-153
View File
@@ -1,153 +0,0 @@
"""Class and functions for storing and retrieving highscores data."""
import os
import json
from classes.guild import Guild # pylint: disable=import-error
from classes.table import Table # pylint: disable=import-error
from classes.category import Category # pylint: disable=import-error
HIGHSCORES_BACKUP_DIR = "deleted_backups"
HIGHSCORES_CONFIG_DIR = "example_config"
HIGHSCORES_DATA_DIR = "highscores_data"
class HighscoresData:
"""Class for storing and retrieving highscores data."""
def __init__(self) -> None:
"""
Default constructor for Highscores Data. Opens up known highscores data on creation.
"""
self._guild_map: dict = {}
# Ensure that data directories exist.
if not os.path.exists(HIGHSCORES_BACKUP_DIR):
os.mkdir(HIGHSCORES_BACKUP_DIR)
if not os.path.exists(HIGHSCORES_DATA_DIR):
os.mkdir(HIGHSCORES_DATA_DIR)
self.open_highscores_data()
def __str__(self) -> str:
"""
Returns Highscores class as a string for printing.
Returns:
str: Highscores class as a string
"""
data: dict = {}
for guild_id, guild_obj in self._guild_map.items():
data[guild_id] = guild_obj.to_dict()
return "Highscores Data\n" + json.dumps(data, indent=4)
@property
def guilds(self) -> "list[Guild]":
"""
Getter for Guilds list.
Returns:
list[Guild]: List of Guilds tracked by Highscores Bot
"""
return self._guild_map.values()
def add_configuration(self, guild_id: int, config_str: str) -> None:
"""
Adds an example configuration to a Guild. Uses guild id to know which guild and
config name to open a file in example_config.
Args:
guild_id (int): Guild id to add config to
config_str (str): Config name to add
"""
config = self._open_highscores_config(config_str)
guild = self.get_guild(guild_id)
for table_name, categorys in config["tables"].items():
table_obj = Table(table_name)
table_obj.size = config["table_size"]
for category_name in categorys:
category = Category(category_name)
category.ascending = config["categories"][category.name]["ascending"]
category.time_record = config["categories"][category.name]["is_time_record"]
table_obj.add_category(category)
guild.add_table(table_obj)
def add_guild(self, guild_id: int) -> None:
"""
Creates a new Guild and adds to Guild map.
Args:
guild_id (int): Guild Id to add
"""
guild: Guild = Guild(guild_id)
self._guild_map[guild_id] = guild
def get_guild(self, guild_id: int) -> Guild:
"""
Getter for Guild object that corresponds with the guild_id.
Args:
guild_id (int): id of Guild to retrieve.
Returns:
Guild: Guild object for guild_id
"""
return self._guild_map[guild_id]
def remove_guild(self, guild_id: int) -> None:
"""
Removes a guild's data from object. Saves backup.
Args:
guild_id (int): guild id to remove
"""
self._guild_map.pop(guild_id)
os.rename(
f"{HIGHSCORES_DATA_DIR}/{guild_id}.json", f"{HIGHSCORES_BACKUP_DIR}/{guild_id}.json"
)
def _open_highscores_config(self, config: str) -> dict:
"""
Opens the highscores config file and returns in dictionary format.
Will be replaced when config class is created.
Args:
config (str): Name of config to load.
Returns:
dict: dict of highscores config
"""
with open(f"{HIGHSCORES_CONFIG_DIR}/{config}.json", encoding="utf-8", mode="r") as file:
return json.load(file)
def open_highscores_data(self):
"""
Opens all saved highscores data in the HIGHSCORES_DATA_DIR dir. Creates a Guild object from
each file and adds to guild map.
"""
for data_file in os.listdir(HIGHSCORES_DATA_DIR):
guild_id: int = int(data_file.split(".", maxsplit=1)[0])
with open(f"{HIGHSCORES_DATA_DIR}/{data_file}", encoding="utf-8", mode="r") as file:
guild_obj: Guild = Guild.from_dict(guild_id, json.load(file))
self._guild_map[guild_obj.guild_id] = guild_obj
def save_all_data(self) -> None:
"""
Calls save_guild_data for each guild tracked in guild map.
"""
for guild in self.guilds:
self.save_guild_data(guild)
def save_guild_data(self, guild: Guild) -> None:
"""
Saves a guild to its own json file.
Args:
guild (Guild): Guild to save
"""
with open(
f"{HIGHSCORES_DATA_DIR}/{guild.guild_id}.json", encoding="utf-8", mode="w"
) as file:
file.write(guild.to_str()) # should use str function
-144
View File
@@ -1,144 +0,0 @@
"""File for Score classes"""
import time
class Score:
"""Parent class for Scores. Stores common information for a Score."""
# no user setter, it should never change.
def __init__(self, user: str) -> None:
"""
Default constructor for Score class.
Args:
user (str): Score's user
value (int): Score's value
"""
self._user: str = user
self._value: int = -1
def __str__(self) -> str:
"""
Returns Score class as a string for printing.
Returns:
str: Score class as a string
"""
return self.to_str()
@property
def user(self) -> str:
"""
Getter for score user.
Returns:
str: user
"""
return self._user
@property
def value(self) -> int:
"""
Getter for score value.
Returns:
int: value
"""
return self._value
@value.setter
def value(self, value: int) -> None:
"""
Setter for score value.
Args:
value (int): score value input
"""
self._value = value
def get_str(self) -> str:
"""
Gets score's value as string.
Returns:
str: Score's value
"""
return str(self._value)
def set_value_str(self, value: str) -> None:
"""
Setter for score value from string.
Args:
value (str): int as a string
"""
self._value = int(value)
def to_str(self) -> str:
"""
Returns Score object as a string.
Returns:
str: string of json representation of Score
"""
return f"{self.user}: {self.get_str()}"
class ScoreTime(Score):
"""Class for a time based score. Inherits from Score."""
def __init__(self, user: str) -> None: # pylint: disable=super-init-not-called
"""
Default constructor for ScoreTime class.
Args:
user (str): Score's user
score (float): Score's value
"""
# store time score as seconds.
self._user: str = user
self._score: float = -1.0
@property
def value(self) -> float:
"""
Getter for score value.
Returns:
float: value
"""
return self._score
@value.setter
def value(self, value: float) -> None:
"""
Setter for score value.
Args:
value (float): value
"""
self._score = value
def get_str(self) -> str:
"""
Gets score's value as string. Turns seconds into MM:SS.ms format.
Returns:
str: value in time format.
"""
milliseconds = round((self.value - int(self.value)) * 10)
return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(self.value))
def set_value_str(self, value: str) -> None:
"""
Setter for score value from a string.
Args:
value (str): string representation of time
"""
if value.find(":"):
self._score = int(value.split(":")[0]) * 60 + float(value.split(":")[1])
else:
self._score = float(value)
-256
View File
@@ -1,256 +0,0 @@
"""
Contains classes and enum for tracking data on submissions.
"""
from enum import Enum
from nextcord import Embed
class SubmissionState(Enum):
"""
Enum for which state a submission is in
"""
CATEGORY = 1
SCORE = 2
PROOF = 3
NAME = 4
SUBMIT = 5
VERIFY = 6
class Submission:
"""
Class for an individual submission. Contains submission information such as
guild id, message id, table, category, username, score and submission state.
"""
def __init__(self, guild_id: int, table: str, username: str) -> None:
"""
Default constructor for Submission. Just need to know guild_id, table, and username.
Score and category are defaulted at empty, message id is 0, and submission state
is SubmissionState.CATEGORY.
"""
self._category: str = ""
self._guild_id: int = guild_id
self._message_id: int = 0
self._score: str = ""
self._submission_state: SubmissionState = SubmissionState.CATEGORY
self._table: str = table
self._username: str = username
@property
def category(self) -> str:
"""
Getter for Submission's Category name.
Returns:
str: Category name.
"""
return self._category
@category.setter
def category(self, category: str) -> None:
"""
Setter for Submission's Category name.
Args:
category (str): Category name
"""
self._category = category
@property
def guild_id(self) -> int:
"""
Getter for guild id Submission is happening in. Will not change.
Returns:
int: guild id
"""
return self._guild_id
@property
def message_id(self) -> int:
"""
Getter for message id that Submission has a view on.
Returns:
int: message id
"""
return self._message_id
@message_id.setter
def message_id(self, m_id: int) -> None:
"""
Setter for message id that Submission has a view on.
Args:
m_id (int): message id
"""
self._message_id = m_id
@property
def score(self) -> str:
"""
Getter for score.
Returns:
str: score
"""
return self._score
@score.setter
def score(self, score: str) -> None:
"""
Setter for score.
Args:
score (str): score
"""
self._score = score
@property
def submission_state(self) -> SubmissionState:
"""
Getter for state Submission is in.
Returns:
SubmissionState: State the Submission is in
"""
return self._submission_state
@submission_state.setter
def submission_state(self, state: SubmissionState) -> None:
"""
Setter for state Submission is in.
Args:
state (SubmissionState): State to change to
"""
self._submission_state = state
@property
def table(self) -> str:
"""
Getter for table Submission is for.
Returns:
str: table name
"""
return self._table
@table.setter
def table(self, table: str) -> None:
"""
Getter for table Submission is for.
Args:
table (str): table name
"""
self._table = table
@property
def username(self) -> str:
"""
Getter for Submission owner's username.
Returns:
str: Submission owner's username
"""
return self._username
@username.setter
def username(self, username: str) -> None:
"""
Setter for Submission owner's username.
Args:
username (str): Submission owner's username
"""
self._username = username
def get_embed(self) -> Embed:
"""Creates an embed based off of the submission state and fields filled out."""
state: SubmissionState = self.submission_state
if state is SubmissionState.CATEGORY:
description = "Step 1: Choose your category!"
elif state is SubmissionState.SCORE:
description = "Step 2: Input your score!"
elif state is SubmissionState.PROOF:
description = "Step 3: Submit your proof!"
elif state is SubmissionState.NAME:
description = (
"Step 4 (Optional): Change name on submission! (proceed to next if skipping)"
)
elif state is SubmissionState.SUBMIT:
description = "Step 5: Submit if everything looks correct!"
elif state is SubmissionState.VERIFY:
description = "Review fields and accept."
embed = Embed(title="Highscores Submission", description=description)
embed.add_field(name="User Name", value=self.username, inline=False)
embed.add_field(name="Table", value=self.table, inline=False)
if self.category:
embed.add_field(name="Category", value=self.category, inline=False)
if self.score:
embed.add_field(name="Score", value=self.score, inline=False)
return embed
class SubmissionMap:
"""
Tracks all active submission.
"""
def __init__(self) -> None:
"""
Default constructor for SubmissionMap. Created empty map.
"""
self._submission_map: dict = {}
def add_submission(self, user_id: int, submission: Submission) -> None:
"""
Adds new submission to submission map. User id is used as a key, a user should only
have one submission at a time.
Args:
user_id (int): User id of submitter. Unique, used as key.
submission (Submission): Submission object
"""
self._submission_map[user_id] = submission
def get_user_submission(self, user_id: int) -> Submission:
"""
Getter for a user's Submission.
Args:
user_id (int): user id
Returns:
Submission: user's submission
"""
return self._submission_map[user_id]
def has_user(self, user_id: int) -> bool:
"""
Tells if user has an active submission in submission map.
Args:
user_id (int): user id
Returns:
bool: bool indicating if user id is in submission map
"""
return user_id in self._submission_map
def remove_user_submission(self, user_id: int) -> None:
"""
Removes a user's Submission. Use when Submission is complete.
Args:
user_id (int): user id to identify user's Submission
"""
self._submission_map.pop(user_id)
-164
View File
@@ -1,164 +0,0 @@
"""File for Table class."""
import json
from classes.category import Category # pylint: disable=import-error
class Table:
"""Stores a table's information a list of categories."""
def __init__(self, name: str) -> None:
"""
Default constructor for Table class.
Creates object and fills out class variables with defaults.
Args:
name (str): Table's name
"""
self._name: str = name
self._message_id: int = 0
self._categories_map: dict = {}
self._size = 3
def __str__(self) -> str:
"""
Returns Table class as a string for printing.
Returns:
str: Table class as a string
"""
return self.to_str()
@classmethod
def from_dict(cls, table_name: str, data: dict):
"""
Creates a Table class from the passed data.
Args:
table_name (str): Table name
data (dict): Rest of Table data in dictionary format
Returns:
Table: Populated Table object
"""
table = cls(table_name)
table.message_id = data["message_id"]
table.size = data["table_size"]
for cat_name, cat in data["categories"].items():
category = Category.from_dict(cat_name, cat)
table.add_category(category)
return table
@property
def categories(self) -> "list[Category]":
"""
Getter for Categories list.
Returns:
list[Category]: List of Categories in this Table
"""
return self._categories_map.values()
@property
def message_id(self) -> int:
"""
Getter for message id
Returns:
int: message id
"""
return self._message_id
@message_id.setter
def message_id(self, m_id: int) -> None:
"""
Setter for message id.
Args:
m_id (int): message id
"""
self._message_id = m_id
@property
def name(self) -> str:
"""
Getter for name.
Returns:
str: name
"""
return self._name
@property
def size(self) -> int:
"""
Getter for size of table.
Returns:
int: Size of table
"""
return self._size
@size.setter
def size(self, size: int) -> None:
"""
Setter for size of table.
Args:
size (int): New size of table
"""
self._size = size
def add_category(self, category: Category) -> None:
"""
Adds a Category to categories map. Shouldn't exist already.
Args:
category (Category): New Category to add
"""
if category.name not in self._categories_map:
self._categories_map[category.name] = category
def get_category(self, category_name: str) -> Category:
"""
Getter for specific category. Returns None if not found.
Args:
category_name (str): name of category to return
Returns:
Category: Category corresponding to name. None if not found.
"""
if category_name in self._categories_map:
return self._categories_map[category_name]
return None
def to_dict(self) -> dict:
"""
Saves Table as a python dictionary.
Does not include table name. It should be the key of above data structure.
Returns:
dict: Table class in dictionary format
"""
data = {}
data["message_id"] = self.message_id
data["table_size"] = self.size
data["categories"] = {}
for category in self.categories:
data["categories"][category.name] = category.to_dict()
return data
def to_str(self) -> str:
"""
Returns Table object as a string.
Returns:
str: string of json representation of Table
"""
return json.dumps(self.to_dict(), indent=4)
-42
View File
@@ -1,42 +0,0 @@
"""
Bot command for deleting the guild's scores.
"""
from nextcord import Embed
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
class DeleteScores(commands.Cog, name="Delete Scores"):
"""
Warning: This command only works for developers.
Deletes this guild's scores.
"""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="delete-scores")
@commands.has_permissions(administrator=True)
async def delete_scores(self, ctx: commands.Context):
"""A command which deletes all scores for this guild.
Usage:
```
?delete_scores
```
"""
# only allow AlwaysCompiling to use.
# If you are a developer and want to use command, can add name to list
if ctx.author.id == 206602713511886849:
for table in highscores_data.get_guild(ctx.guild.id).tables:
for category in table.categories:
category.clear()
highscores_data.save_guild_data(highscores_data.get_guild(ctx.guild.id))
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(DeleteScores(bot))
-43
View File
@@ -1,43 +0,0 @@
"""
Defines cog for guild listeners. Contains functions for various guild events.
"""
from nextcord.ext import commands
import nextcord
from highscores import highscores_data # pylint: disable=import-error
class GuildListener(commands.Cog, name="Guild Listener"):
"""Sets up message listener."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild):
"""Listener for Guild Join. Creates skeleton for new guild joined and saves it."""
highscores_data.add_guild(guild.id)
# This add config will be removed eventually. Config adding will have to be controlled by commands.
highscores_data.add_configuration(guild.id, "runescape")
highscores_data.save_guild_data(highscores_data.get_guild(guild.id))
welcome_message: str = "Hello, I keep track of Highscores for you!"
welcome_message += " Currently only supporting RuneScape boss highscores,"
welcome_message += " but will be more dynamic support in future."
welcome_message += ' For assistance on getting started, use "?register help".'
await guild.system_channel.send(content=welcome_message)
@commands.Cog.listener()
async def on_guild_remove(self, guild: nextcord.Guild):
"""Listener for Guild Remove. Removes data from highscores_data."""
print(f"Removing data for guild {guild.name} (id {guild.id}).")
highscores_data.remove_guild(guild.id)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(GuildListener(bot))
-26
View File
@@ -1,26 +0,0 @@
"""
Help command
"""
from nextcord.ext import commands
from .help_command import NewHelpCommand
class HelpCog(commands.Cog):
"""Help Command"""
def __init__(self, bot: commands.Bot):
self.bot = bot
self._original_help_command = bot.help_command
bot.help_command = NewHelpCommand()
bot.help_command.cog = self
def cog_unload(self):
self.bot.help_command = self._original_help_command
# setup functions for bot
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(HelpCog(bot))
-84
View File
@@ -1,84 +0,0 @@
"""
Help command. Copied from discord-bot-template.
"""
import nextcord
from nextcord.embeds import EmptyEmbed
from nextcord.ext import commands
import config # pylint: disable=import-error
class NewHelpCommand(commands.MinimalHelpCommand):
"""Custom help command override using embeds"""
COLOUR = nextcord.Colour.blurple()
def get_ending_note(self):
"""Returns note to display at the bottom"""
invoked_with = self.invoked_with
return f"Use {config.BOT_PREFIX}{invoked_with} [command] for more info on a command."
def get_command_signature(self, command: commands.core.Command):
"""Retrieves the signature portion of the help page."""
return f"{command.qualified_name} {command.signature}"
async def send_bot_help(self, mapping: dict):
"""implements bot command help page"""
embed = nextcord.Embed(title="Bot Commands", colour=self.COLOUR)
avatar = self.context.bot.user.avatar
avatar_url = avatar.url if avatar else EmptyEmbed
embed.set_author(name=self.context.bot.user.name, icon_url=avatar_url)
description = self.context.bot.description
if description:
embed.description = description
for cog, commands in mapping.items(): # pylint: disable=redefined-outer-name
name = "No Category" if cog is None else cog.qualified_name
filtered = await self.filter_commands(commands, sort=True)
if filtered:
# \u2002 = middle dot
value = "\u2002".join(f"{config.BOT_PREFIX}{c.name}" for c in filtered)
if cog and cog.description:
value = f"{cog.description}\n{value}"
embed.add_field(name=name, value=value)
embed.set_footer(text=self.get_ending_note())
await self.get_destination().send(embed=embed)
async def send_cog_help(self, cog: commands.Cog):
"""implements cog help page"""
embed = nextcord.Embed(title=f"{cog.qualified_name} Commands", colour=self.COLOUR)
if cog.description:
embed.description = cog.description
filtered = await self.filter_commands(cog.get_commands(), sort=True)
for command in filtered:
embed.add_field(
name=self.get_command_signature(command),
value=command.short_doc or "...",
inline=False,
)
embed.set_footer(text=self.get_ending_note())
await self.get_destination().send(embed=embed)
async def send_group_help(self, group: commands.Group):
"""implements group help page and command help page"""
embed = nextcord.Embed(title=group.qualified_name, colour=self.COLOUR)
if group.help:
embed.description = group.help
if isinstance(group, commands.Group):
filtered = await self.filter_commands(group.commands, sort=True)
for command in filtered:
embed.add_field(
name=self.get_command_signature(command),
value=command.short_doc or "...",
inline=False,
)
embed.set_footer(text=self.get_ending_note())
await self.get_destination().send(embed=embed)
# Use the same function as group help for command help
send_command_help = send_group_help # type: ignore
-115
View File
@@ -1,115 +0,0 @@
"""
Defines cog for on message listener.
"""
from datetime import date
import random
import re
from nextcord.ext import commands
import nextcord
from classes.submission import SubmissionState # pylint: disable=import-error
from highscores import highscores_data, submission_objects # pylint: disable=import-error
class SubmissionListener(commands.Cog, name="Message Listener"):
"""Sets up message listener."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_message(self, message: nextcord.Message):
"""Listener for message creation. Adds score and attachments to submission."""
# don't want to process any messages from bot.
if message.author.id == self.bot.user.id:
return
guild_id = message.guild.id
# troll addition for friends birthday
if guild_id == 239758371358638080 and date.today() == date(2022, 7, 31):
# 5% chance to send the message
chance = random.randint(1, 20)
if chance == 1:
await message.channel.send("Happy Birthday Chris <@642330132958281750>")
if submission_objects.has_user(message.author.id):
submission = submission_objects.get_user_submission(message.author.id)
message_id = submission.message_id
submission_channel_id = highscores_data.get_guild(guild_id).submission_channel
submission_message: nextcord.Message = (
await self.bot.get_guild(guild_id)
.get_channel(submission_channel_id)
.fetch_message(message_id)
)
# need check if correct user and correct channel
if message.channel.id == submission_channel_id:
state = submission.submission_state
if state == SubmissionState.SCORE and message.content:
# check with regex if score matches correctly
is_time_record = (
highscores_data.get_guild(guild_id)
.get_table(submission.table)
.get_category(submission.category)
.time_record
)
time_regex = re.search(
"^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", message.content
)
int_regex = re.search("^[0-9]+$", message.content)
if time_regex is None and is_time_record:
embed = submission_message.embeds[0]
embed.add_field(
name="Error",
value="Time record must follow format MM:SS.ms, SS.ms, or SS.",
)
elif int_regex is None and not is_time_record:
embed = submission_message.embeds[0]
embed.add_field(name="Error", value="Number record must only be number.")
else:
score = message.content
if is_time_record:
# standardize time score format.
if re.search("^[1-9]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# score is already fine.
pass
elif re.search("^[0-9]{2}(\\.[0-9])?$", score):
# need to add leading colon and 0
score = "0:" + score
elif re.search("^[0-9](\\.[0-9])?$", score):
# need to add 0 in seconds and leading colon and 0
score = "0:0" + score
elif re.search("^[0]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# if there is a minute 10ths place 0, remove
score = score.lstrip("0")
submission.score = score
embed = submission.get_embed()
await submission_message.edit(
embed=embed,
)
elif state == SubmissionState.PROOF and len(message.attachments) > 0:
await submission_message.edit(
files=[await attch.to_file() for attch in message.attachments],
)
elif state == SubmissionState.NAME and message.content:
submission.username = message.content[
: highscores_data.get_guild(guild_id).username_length
]
embed = submission.get_embed()
await submission_message.edit(
embed=embed,
)
await message.delete()
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(SubmissionListener(bot))
-57
View File
@@ -1,57 +0,0 @@
"""
Bot command to post the highscores
"""
from nextcord.ext import commands
import nextcord
from cogs.submission.submission_views import SubmissionCreateButton # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error
from utilities.utils import send_highscore_message # pylint: disable=import-error
class PostHighscores(commands.Cog, name="Post Highscores"):
"""Posts Highscores."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(name="post-scores")
@commands.has_permissions(administrator=True)
async def post_highscores(self, ctx: commands.Context):
"""
A command which posts the highscore in the designated channel.
Sends warning if no channel has been registered.
Usage:
```
?post-scores
```
"""
guild = highscores_data.get_guild(ctx.guild.id)
print(f"Printing out highscores information in channel {guild.highscore_channel}")
channel = ctx.guild.get_channel(guild.highscore_channel)
error_response = "Registered Highscores channel does not exist or was never registered. "
error_response += 'Register with "?register" command.'
assert channel is not None, await ctx.send(error_response)
if len(highscores_data.get_guild(ctx.guild.id).tables) < 1:
await channel.send(
"No tables have been created for this guild. Please create some tables."
)
return
for table in highscores_data.get_guild(ctx.guild.id).tables:
message: nextcord.Message = await send_highscore_message(channel, guild, table)
await message.edit(view=SubmissionCreateButton(self.bot))
# have to save data here in case message id was changed. rather save here after all
# messages have been sent.
highscores_data.save_guild_data(guild)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(PostHighscores(bot))
-53
View File
@@ -1,53 +0,0 @@
"""
Functions for controlling the bot's presence.
"""
import random
from nextcord.ext import commands
from nextcord.ext import tasks
import nextcord
class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild): # pylint: disable=unused-argument
"""Listener for Guild Join. Updated presence on guild join."""
await self.change_presence()
@commands.Cog.listener()
async def on_ready(self):
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_change_presense is subclassed from tasks.loop.
self.periodic_change_presence.start() # pylint: disable=no-member
@tasks.loop(minutes=2)
async def periodic_change_presence(self):
"""Loops every 2 minutes and changes presence."""
await self.change_presence()
async def change_presence(self):
"""Randomly chooses a presence and updates bot's presence."""
statuses = [
f"?help | {len(self.bot.guilds)} servers",
f"Vuln Pls. | {len(self.bot.guilds)} servers",
f"All your base are belong to us. | {len(self.bot.guilds)} servers",
f"Double plus good. | {len(self.bot.guilds)} servers",
]
status = random.choice(statuses)
await self.bot.change_presence(activity=nextcord.Game(status))
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(BotPresence(bot))
-69
View File
@@ -1,69 +0,0 @@
"""
Bot command for registering various channels to the bot.
"""
from nextcord import Embed
from nextcord.ext import commands
from highscores import highscores_data # pylint: disable=import-error
class RegisterVerificationChannel(commands.Cog, name="Register"):
"""Bot command for registering various channels to the bot."""
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@commands.command(name="register")
@commands.has_permissions(administrator=True)
async def register_highscores(self, ctx: commands.Context, registration_type: str) -> None:
"""A command which registers current channel as for various uses.
Usage:
```
?register verify (Use in channel you want to verify submissions in.)
?register highscore (Use in channel you want to show highscore messages in.)
?register submission (Use in channel you want submission messages to be send it.)
?register help (Shows help message.)
```
"""
# respond to the message
channel_id = ctx.channel.id
guild = highscores_data.get_guild(ctx.guild.id)
if registration_type == "verify":
await ctx.send(f"Registered <#{channel_id}> as channel to post verifications in.")
guild.verification_channel = channel_id
elif registration_type == "highscore":
await ctx.send(f"Registered <#{channel_id}> as channel to post highscores in.")
guild.highscore_channel = channel_id
elif registration_type == "submission":
await ctx.send(f"Registered <#{channel_id}> as channel to post submissions in.")
guild.submission_channel = channel_id
else:
embed = Embed(
title="Register command help", description="How to use the register command."
)
embed.add_field(
name="?register verify", value="Use in channel you want to verify submissions in."
)
embed.add_field(
name="?register highscore",
value="Use in channel you want to show highscore messages in.",
)
embed.add_field(
name="?register submission",
value="Use in channel you want submission messages to be send it.",
)
embed.add_field(name="?register help", value="Use to show this message.")
await ctx.reply(embed=embed)
# return, don't need to save data
return
highscores_data.save_guild_data(guild)
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot) -> None:
"""Adds function to bot"""
bot.add_cog(RegisterVerificationChannel(bot))
-231
View File
@@ -1,231 +0,0 @@
"""Views used for submission process."""
from typing import Optional
import nextcord
from nextcord.ext import commands
from classes.submission import Submission, SubmissionState # pylint: disable=import-error
from highscores import ( # pylint: disable=import-error
highscores_data,
highscores_message_map,
submission_objects,
)
from .verification_view import VerificationView
def category_select_options(guild_id: int, boss_name: str) -> "list[nextcord.SelectOption]":
"""Returns list of categories for a given boss."""
options: "list[nextcord.SelectOption]" = []
category_list = highscores_data.get_guild(guild_id).get_table(boss_name).categories
for category in category_list:
options.append(nextcord.SelectOption(label=category.name))
return options
############ Items used in submission process ############
class SubmissionDropdown(nextcord.ui.Select):
"""Defines dropdown menu for submission"""
def __init__(
self,
bot: commands.Bot,
options: "list[nextcord.SelectOption]",
):
super().__init__(
placeholder="Choose submission category.", min_values=1, max_values=1, options=options
)
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
submission = submission_objects.get_user_submission(interaction.user.id)
submission.category = self.values[0]
embed = submission.get_embed()
await interaction.message.edit(
embed=embed,
)
class BackButton(nextcord.ui.Button):
"""Defines view for back button."""
def __init__(self, bot: commands.Bot):
super().__init__(label="Back", style=nextcord.ButtonStyle.primary, custom_id="back")
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
submission = submission_objects.get_user_submission(interaction.user.id)
state: SubmissionState = submission.submission_state
state = SubmissionState(state.value - 1)
submission.submission_state = state
embed = submission.get_embed()
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
class CancelButton(nextcord.ui.Button):
"""Deletes message to cancel interaction.."""
def __init__(self):
super().__init__(label="Cancel", style=nextcord.ButtonStyle.red, custom_id="cancel")
async def callback(self, interaction: nextcord.Interaction):
submission_objects.remove_user_submission(interaction.user.id)
await interaction.message.delete()
class NextButton(nextcord.ui.Button):
"""Defines view for next button."""
def __init__(self, bot: commands.Bot):
super().__init__(label="Next", style=nextcord.ButtonStyle.primary, custom_id="next")
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
# check if fields have been filled out for each state
submission = submission_objects.get_user_submission(interaction.user.id)
state: SubmissionState = submission.submission_state
if state == SubmissionState.CATEGORY and not submission.category:
await interaction.send("Please choose a category.", ephemeral=True)
elif state == SubmissionState.SCORE and not submission.score:
await interaction.send("Please send a score.", ephemeral=True)
elif state == SubmissionState.PROOF and not len(interaction.message.attachments) > 0:
await interaction.send("Please attach picture for proof.", ephemeral=True)
else:
state = SubmissionState(state.value + 1)
submission.submission_state = state
embed = submission.get_embed()
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
class SubmitButton(nextcord.ui.Button):
"""Defines button to submit a user's score."""
def __init__(self, bot: commands.Bot):
super().__init__(label="Submit", style=nextcord.ButtonStyle.green, custom_id="submit")
self._bot = bot
async def callback(self, interaction: nextcord.Interaction):
verification_channel_id = highscores_data.get_guild(
interaction.guild_id
).verification_channel
channel = interaction.guild.get_channel(verification_channel_id)
submission = submission_objects.get_user_submission(interaction.user.id)
error_response = "Registered Verification channel does not exist or was never registered. \
Contact server admins."
assert channel is not None, await channel.send(error_response)
submission.submission_state = SubmissionState.VERIFY
embed = submission.get_embed()
await channel.send(
files=[await attch.to_file() for attch in interaction.message.attachments],
view=VerificationView(),
embed=embed,
content=f"Submission from {interaction.user.display_name}",
)
await interaction.message.edit(content=f"<@{interaction.user.id}> Submitted!", view=None)
submission_objects.remove_user_submission(interaction.user.id)
############ Submission Views ############
class SubmissionCreateButton(nextcord.ui.View):
"""Defines view for score submission button."""
def __init__(self, bot: commands.Bot):
super().__init__(timeout=None)
self._bot = bot
@nextcord.ui.button(
label="Submit", style=nextcord.ButtonStyle.primary, custom_id="submitcreate"
)
async def submitcreate_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for score submission."""
# Don't allow a user to start a new submission if one already exists
if submission_objects.has_user(interaction.user.id):
await interaction.send("Please finish in progress submission first.", ephemeral=True)
return
table = highscores_message_map[interaction.guild.id][interaction.message.id]
channel = interaction.guild.get_channel(
highscores_data.get_guild(interaction.guild_id).submission_channel
)
# a user should only have 1 submission active at once.
# split username on pipe then limit length.
username: str = interaction.user.display_name.split("|")[0][
: highscores_data.get_guild(interaction.guild_id).username_length
]
submission: Submission = Submission(interaction.guild_id, table, username)
submission_objects.add_submission(interaction.user.id, submission)
embed = submission.get_embed()
submission_message = await channel.send(
f"<@{interaction.user.id}>",
view=SubmissionView(
self._bot,
interaction.user.id,
),
embed=embed,
)
# update the message_id once its created
submission.message_id = submission_message.id
class SubmissionView(nextcord.ui.View):
"""Defines view submitting. Adjusts to all states."""
def __init__(
self,
bot: commands.Bot,
submission_user: int,
*,
timeout: Optional[float] = 300,
):
super().__init__(timeout=timeout)
self._submission_user = submission_user
self._bot = bot
self._state = submission_objects.get_user_submission(self._submission_user).submission_state
self._boss_name = submission_objects.get_user_submission(self._submission_user).table
self._guild_id = submission_objects.get_user_submission(self._submission_user).guild_id
# add items depending on given state
if self._state is SubmissionState.CATEGORY:
self.add_item(
SubmissionDropdown(bot, category_select_options(self._guild_id, self._boss_name))
)
self.add_item(CancelButton())
self.add_item(NextButton(self._bot))
elif (
self._state is SubmissionState.SCORE
or self._state is SubmissionState.PROOF
or self._state is SubmissionState.NAME
):
self.add_item(BackButton(self._bot))
self.add_item(NextButton(self._bot))
elif self._state is SubmissionState.SUBMIT:
self.add_item(BackButton(self._bot))
self.add_item(SubmitButton(self._bot))
async def interaction_check(self, interaction: nextcord.Interaction) -> bool:
check = self._submission_user == interaction.user.id
if not check:
await interaction.send("This is not your submission.", ephemeral=True)
return check
async def on_timeout(self) -> None:
# remove user from submission objects after time out. Allows user to start new submission.
submission_objects.remove_user_submission(self._submission_user)
-65
View File
@@ -1,65 +0,0 @@
"""View for verification buttons"""
import nextcord
from classes.score import Score, ScoreTime # pylint: disable=import-error
from highscores import highscores_data # pylint: disable=import-error
from utilities.utils import submit_score # pylint: disable=import-error
class VerificationView(nextcord.ui.View):
"""Defines views for verifying score submissions"""
def __init__(self):
super().__init__(timeout=None)
@nextcord.ui.button(
label="Deny", style=nextcord.ButtonStyle.red, custom_id="deny-button-submission"
)
async def deny_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for denying score submission."""
await interaction.message.delete()
@nextcord.ui.button(
label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission"
)
async def approve_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for approving score submission."""
content = interaction.message.embeds[0].to_dict()
for field in content["fields"]:
if field["name"] == "Table":
table = field["value"]
elif field["name"] == "Category":
category = field["value"]
elif field["name"] == "Score":
score = field["value"]
elif field["name"] == "User Name":
user = field["value"]
try:
submission_guild = highscores_data.get_guild(interaction.guild_id)
submission_table = submission_guild.get_table(table)
submission_category = submission_table.get_category(category)
if submission_category.time_record:
submission_score: ScoreTime = ScoreTime(user)
submission_score.set_value_str(score)
else:
submission_score: Score = Score(user)
submission_score.set_value_str(score)
await submit_score(
interaction,
submission_guild,
submission_table,
submission_category,
submission_score,
)
await interaction.message.edit(f"Approved by <@{interaction.user.id}>.", view=None)
except UnboundLocalError:
await interaction.send("Instruct user to submit with all fields.")
await interaction.message.edit(view=None)
-9
View File
@@ -1,9 +0,0 @@
import os
from dotenv.main import load_dotenv
load_dotenv()
# Discord config
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
BOT_PREFIX = "?"
-335
View File
@@ -1,335 +0,0 @@
{
"tables": {
"The Ambassador": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Araxxi": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Arch-Glacor": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Enrage",
"Spooned Log"
],
"Astellarn": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"The Barrows Brothers": [
"Kill Count",
"Spooned Log"
],
"The Barrows: Rise of the Six": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Beastmaster Durzag": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Black Stone Dragon": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Chaos Elemental": [
"Kill Count",
"Spooned Log"
],
"Commander Zilyana": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Corporeal Beast": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Crassian Leviathan": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Croesus": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Dagannoth Kings": [
"Kill Count",
"Spooned Log"
],
"General Graardor": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Giant Mole": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Gregorovic": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Har-Aken": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Helwyr": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Kalphite King": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Kalphite Queen": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Kerapac": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"King Black Dragon": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Kree'arra": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"K'ril Tsutsaroth": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Legiones": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"The Magister": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Masuta the Ascended": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Nex": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Nex - Angel of Death": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Queen Black Dragon": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Raksha": [
"Solo Kill Count",
"Solo Kill Time",
"Duo Kill Count",
"Duo Kill Time",
"Spooned Log"
],
"Rex Matriarchs": [
"Kill Count",
"Spooned Log"
],
"The Sanctum Guardian": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Seiryu the Azure Serpent": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Solak": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Taraket the Necromancer": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Telos": [
"Kill Count",
"Kill Time",
"Enrage",
"Spooned Log"
],
"The Twin Furies": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"TzKal-Zuk": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"TzTok-Jad": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Verak Lith": [
"Solo Kill Count",
"Solo Kill Time",
"Group Kill Count",
"Group Kill Time",
"Spooned Log"
],
"Vindicta & Gorvek": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Vorago": [
"Kill Count",
"Kill Time",
"Hardmode Kill Count",
"Hardmode Kill Time",
"Spooned Log"
],
"Yakamaru": [
"Kill Count",
"Kill Time",
"Spooned Log"
],
"Zamorak, Lord of Chaos (solo)": [
"Kill Count",
"Kill Time",
"Enrage",
"Spooned Log"
],
"Zamorak, Lord of Chaos (group)": [
"Kill Count",
"Kill Time",
"Enrage"
]
},
"categories": {
"Duo Kill Count": {
"ascending": false,
"is_time_record": false
},
"Duo Kill Time": {
"ascending": true,
"is_time_record": true
},
"Enrage": {
"ascending": false,
"is_time_record": false
},
"Group Kill Count": {
"ascending": false,
"is_time_record": false
},
"Group Kill Time": {
"ascending": true,
"is_time_record": true
},
"Hardmode Kill Count": {
"ascending": false,
"is_time_record": false
},
"Hardmode Kill Time": {
"ascending": true,
"is_time_record": true
},
"Kill Count": {
"ascending": false,
"is_time_record": false
},
"Kill Time": {
"ascending": true,
"is_time_record": true
},
"Solo Kill Count": {
"ascending": false,
"is_time_record": false
},
"Solo Kill Time": {
"ascending": true,
"is_time_record": true
},
"Spooned Log": {
"ascending": true,
"is_time_record": false
}
},
"table_size": 3
}
-74
View File
@@ -1,74 +0,0 @@
"""
main file for pvm-highscores bot
"""
import logging
import os
import sys
import nextcord
from nextcord.ext import commands
import config
from classes.highscores_data import HighscoresData
from classes.submission import SubmissionMap
logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
logger.addHandler(handler)
highscores_data: HighscoresData = HighscoresData()
highscores_message_map = {}
for guild in highscores_data.guilds:
highscores_message_map[guild.guild_id] = {}
for table in guild.tables:
highscores_message_map[guild.guild_id][table.message_id] = table.name
submission_objects: SubmissionMap = SubmissionMap()
def main():
"""Main function for bot."""
min_python_version = (3, 7)
if sys.version_info < min_python_version:
print("ERROR: Python version must be >= 3.7")
sys.exit()
# Allows privledged intents for monitoring members joining, roles editing, and role assignments
# These need to be enabled in the developer portal as well
intents = nextcord.Intents.default()
# Required in order to read messages (eg. prefix commands)
intents.message_content = True # pylint: disable=assigning-non-slot
# To enable the guilds priveleged intent:
# intents.guilds = True
# To enable the members priveleged intent:
# intents.members = True
bot = commands.Bot(
commands.when_mentioned_or(config.BOT_PREFIX),
intents=intents,
)
# Get the modules of all cogs whose directory structure is ./cogs/<module_name>
for folder in os.listdir("cogs"):
bot.load_extension(f"cogs.{folder}")
@bot.listen()
async def on_ready():
"""When Discord is connected"""
assert bot.user is not None
print(f"{bot.user.name} has connected to Discord!")
# Run Discord bot
bot.run(config.DISCORD_TOKEN)
if __name__ == "__main__":
main()
+124
View File
@@ -0,0 +1,124 @@
[project]
name = "agon"
version = "0.1.0"
description = "A highscores discord bot"
authors = [{ name = "imcompiling", email = "<[email protected]>" }]
readme = "README.md"
dependencies = [
"cryptography>=43.0.3",
"nextcord>=2.6.0",
"pandas>=2.2.3",
"pymysql>=1.1.1",
"python-dotenv>=1.0.1",
"setuptools>=75.3.0",
"sqlmodel>=0.0.22",
]
requires-python = ">=3.12"
[tool.uv]
dev-dependencies = ["ruff>=0.7.2"]
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
# Same as Black.
line-length = 120
indent-width = 4
# Assume Python 3.8
target-version = "py312"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["ALL"]
ignore = [
"B008", # I build SlashOptions as arguments.
"COM812",
"COM819",
"D107", # don't want docstrings on init functions
"D203",
"D206",
"D212",
"D300",
"E111",
"E114",
"E117",
"ISC001",
"ISC002",
"PD011", # doesn't like me using '.values' on embeds....
"Q000",
"Q001",
"Q002",
"Q003",
"W191",
]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.flake8-annotations]
mypy-init-return = true
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = true
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
+29
View File
@@ -0,0 +1,29 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"lockFileMaintenance": {
"automerge": true
},
"major": {
"automerge": false,
"automergeType": "pr"
},
"minor": {
"automerge": true,
"automergeType": "branch"
},
"patch": {
"automerge": true,
"automergeType": "branch"
},
"pin": {
"automerge": true,
"automergeType": "branch"
},
"ignoreTests": true,
"dockerfile": {
"minor": {
"automerge": false,
"automergeType": "pr"
}
}
}
-2
View File
@@ -1,2 +0,0 @@
nextcord>=2.0.0rc1
python-dotenv>=0.20.0
-123
View File
@@ -1,123 +0,0 @@
"""
Contains various utlity functions for highscores bot
"""
import nextcord
from highscores import highscores_data, highscores_message_map # pylint: disable=import-error
from classes.score import Score # pylint: disable=import-error
from classes.guild import Guild # pylint: disable=import-error
from classes.category import Category # pylint: disable=import-error
from classes.table import Table # pylint: disable=import-error
def format_highscore_message(guild: Guild, table: Table) -> str:
"""
Creates a formatted string for a boss's highscore discord message.
This formatted string is in discord's code block to ensure monospace text.
Loops through each category to build the message one place at a time.
Args:
guild (Guild): Guild object that is posting the message.
table (Table): Table object that contains the contents of the message.
Returns:
str: Formatted string for discord message
"""
ret_string = f"**{table.name}**\n```"
# Gather all categories and add to string
for category in table.categories:
ret_string += f"{category.name}".ljust(26, " ")
ret_string += "\n"
# list rankings in a single line. Start with first place for each category and then continue
for i in range(table.size):
for category in table.categories:
if len(category.scores) > i:
# pad username to max length.
# have to convert category.scores into list to make it subscriptable. no idea why.
username: str = list(category.scores)[i].user.ljust(guild.username_length, " ")
score: str = list(category.scores)[i].get_str()
# pad score to correct length
# MM:SS.ms is 7 characters long
if category.time_record:
# if there are single digit minutes, add a space for formatting.
if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2:
score = " " + score
else:
score = score.rjust(7, " ")
ret_string += f"{i+1}: {username} - {score}".ljust(26, " ")
else:
ret_string += f"{i+1}: submit your score".ljust(26, " ")
ret_string += "\n"
return ret_string + "```"
async def send_highscore_message(
channel: nextcord.TextChannel, guild: Guild, table: Table
) -> nextcord.Message:
"""
Function takes care of checking if a message exists for a table.
Creates and sends/edits the message.
Args:
channel (nextcord.TextChannel): Discord channel object where message is sent
guild (Guild): Guild object for Guild that is posting the message
table (Table): Table to post to the message
Returns:
nextcord.Message: Message object that was posted with table contents
"""
highscore_string = format_highscore_message(guild, table)
message_id = table.message_id
try:
message: nextcord.Message = await channel.fetch_message(message_id)
await message.edit(content=highscore_string)
except nextcord.NotFound:
# message doesn't exist.
message: nextcord.Message = await channel.send(highscore_string)
message_id = message.id
table.message_id = message_id
# save message -> boss_name in message_map
# get guild id from channel
# channel should come from the guild that is requesting this highscores message anyways.
# check if guild is in message map already
if not channel.guild.id in highscores_message_map:
highscores_message_map[channel.guild.id] = {}
highscores_message_map[channel.guild.id][message_id] = table.name
return message
async def submit_score(
interaction: nextcord.Interaction, guild: Guild, table: Table, category: Category, score: Score
) -> None:
"""
Adds passed score to table. Then updates table message by calling send_highscore_message.
Ensures that a highscores channel exists.
Args:
interaction (nextcord.Interaction): Interaction that caused the score submission
guild (Guild): Guild to submit score to
table (Table): Table to submit score to
category (Category): Category to submit score to
score (Score): Score to submit
"""
category.add_score(score)
highscores_data.save_guild_data(guild)
# edit message
print(f"Editting message {guild.highscore_channel}")
channel = interaction.guild.get_channel(guild.highscore_channel)
error_response = "Registered Highscores channel does not exist or was never registered."
error_response += 'Register with "?register" command.'
assert channel is not None, await interaction.send(error_response, ephemeral=True)
await send_highscore_message(channel, guild, table)
Generated
+676
View File
@@ -0,0 +1,676 @@
version = 1
requires-python = ">=3.12"
[[package]]
name = "agon"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "cryptography" },
{ name = "nextcord" },
{ name = "pandas" },
{ name = "pymysql" },
{ name = "python-dotenv" },
{ name = "setuptools" },
{ name = "sqlmodel" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "cryptography", specifier = ">=43.0.3" },
{ name = "nextcord", specifier = ">=2.6.0" },
{ name = "pandas", specifier = ">=2.2.3" },
{ name = "pymysql", specifier = ">=1.1.1" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "setuptools", specifier = ">=75.3.0" },
{ name = "sqlmodel", specifier = ">=0.0.22" },
]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.7.2" }]
[[package]]
name = "aiohappyeyeballs"
version = "2.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/69/2f6d5a019bd02e920a3417689a89887b39ad1e350b562f9955693d900c40/aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", size = 21809 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572", size = 14742 },
]
[[package]]
name = "aiohttp"
version = "3.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
{ name = "aiosignal" },
{ name = "attrs" },
{ name = "frozenlist" },
{ name = "multidict" },
{ name = "propcache" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/77/07/4d2eb92798dc636e9512051b7fcfd3e6032a15c70af852abe777757e4c2d/aiohttp-3.11.0.tar.gz", hash = "sha256:f57a0de48dda792629e7952d34a0c7b81ea336bb9b721391c7c58145b237fe55", size = 7656057 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/a1/ed155cbbe9b518e4af3f7bcb914763962a6ffffcfe703915c77bdf5c3790/aiohttp-3.11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f8f0d79b923070f25674e4ea8f3d61c9d89d24d9598d50ff32c5b9b23c79a25b", size = 701102 },
{ url = "https://files.pythonhosted.org/packages/da/e2/d3e01a6ad245426cd819a48483db82000f110c6438fc7f41cda3cefa02e6/aiohttp-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:113bf06b029143e94a47c4f36e11a8b7e396e9d1f1fc8cea58e6b7e370cfed38", size = 461039 },
{ url = "https://files.pythonhosted.org/packages/8d/9e/1bd08f529222c0dd4301a06894c3beb4784e8ef3179afee8b6860869bc92/aiohttp-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3e1ed8d152cccceffb1ee7a2ac227c16372e453fb11b3aeaa56783049b85d3f6", size = 453661 },
{ url = "https://files.pythonhosted.org/packages/fe/03/ffab64af75db3a8f33c8701cba25e06e0fb1858d050f8698077ecaece256/aiohttp-3.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2e82e515e268b965424ecabebd91834a41b36260b6ef5db015ee12ddb28ef3", size = 1676578 },
{ url = "https://files.pythonhosted.org/packages/a2/23/22f2b6d1f57ddade6f7d6630f39cbab02e6be9a62b09b9b6874c1f769ae4/aiohttp-3.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1c49bc393d854d4421ebc174a0a41f9261f50d3694d8ca277146cbbcfd24ee7", size = 1731906 },
{ url = "https://files.pythonhosted.org/packages/c0/e8/4f02ebea5f1032b382bb150a843ceb348b10f536c371e320892d1447ea98/aiohttp-3.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57e17c6d71f2dc857a8a1d09be1be7802e35d90fb4ba4b06cf1aab6414a57894", size = 1786772 },
{ url = "https://files.pythonhosted.org/packages/f8/db/62ce138bd83b5905b2d368dd5f2eae93599c2c37586f2907acecd767c0e1/aiohttp-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12071dd2cc95ba81e0f2737bebcb98b2a8656015e87772e84e8fb9e635b5da6e", size = 1683964 },
{ url = "https://files.pythonhosted.org/packages/93/6c/aee13936697dfb4ea879a9acb6d26a2012d28e2142ef6f92d706d8f4cd3d/aiohttp-3.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97056d3422594e0787733ac4c45bef58722d452f4dc6615fee42f59fe51707dd", size = 1616545 },
{ url = "https://files.pythonhosted.org/packages/ed/4d/5948c19f61e3e0c5a2a922ad32cff4614a89172b07f4a69d4c1169f79007/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ec5efbc872b00ddd85e3904059d274f284cff314e13f48776050ca2c58f451d", size = 1634428 },
{ url = "https://files.pythonhosted.org/packages/b7/5a/b78ffab91436ccc9a82b4f43b24db2f70e9cabd19f414b417f25ca455060/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd505a1121ad5b666191840b7bd1d8cb917df2647deeca6f3474331b72452362", size = 1647244 },
{ url = "https://files.pythonhosted.org/packages/6d/8a/459335e065715b92f8448a213f744526f2c0c4d5b78986092b10458af501/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:600b1d9f86a130131915e2f2127664311b33902c486b21a747d626f5144b4471", size = 1695006 },
{ url = "https://files.pythonhosted.org/packages/1e/bd/9e2d7b0a192ecccdd46d042b9fb023db62e5de1fdf371c5b60fd5239c3d3/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8c47a0ba6c2b3d3e5715f8338d657badd21f778c6be16701922c65521c5ecfc9", size = 1726262 },
{ url = "https://files.pythonhosted.org/packages/02/b2/c96436f71beafc3304e8de25942a2f3462358c018ebd31b1dada8cf6392d/aiohttp-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b323b5d3aef7dd811424c269322eec58a977c0c8152e650159e47210d900504", size = 1687536 },
{ url = "https://files.pythonhosted.org/packages/75/3b/39f789385d12987daa4a2dd736b796c9770e03adda8602991bbae0c9c3a1/aiohttp-3.11.0-cp312-cp312-win32.whl", hash = "sha256:aabc4e92cb153636d6be54e84dad1b252ddb9aebe077942b6dcffe5e468d476a", size = 409129 },
{ url = "https://files.pythonhosted.org/packages/68/69/de71e1abde4340e9ea0b0ba67f56a4784ef3b4759d89cc07654487cc5ee1/aiohttp-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:508cfcc99534b1282595357592d8367b44392b21f6eb5d4dc021f8d0d809e94d", size = 435694 },
{ url = "https://files.pythonhosted.org/packages/f8/9a/768cea33c03a832a98292858a7bf2ec87f50b27bd8996eaafc5e9de96625/aiohttp-3.11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c98a596ac20e8980cc6f34c0c92a113e98eb08f3997c150064d26d2aeb043e5a", size = 694285 },
{ url = "https://files.pythonhosted.org/packages/05/e1/ae1a1c48e74b1a8cd2c3a0d9b872f7feae9276515a6bce29c1a7b4120e39/aiohttp-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ad14cdc0fba4df31c0f6e06c21928c5b924725cbf60d0ccc5f6e7132636250e9", size = 457450 },
{ url = "https://files.pythonhosted.org/packages/a6/30/b5b443ab48050daa0e6506c5974869bc8f37c48494d829ab83cef7239bf9/aiohttp-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:170fb2324826bb9f08055a8291f42192ae5ee2f25b2966c8f0f4537c61d73a7b", size = 450571 },
{ url = "https://files.pythonhosted.org/packages/12/32/fdf0f2754c9a51867b54fb4ebfc05445cfb9405d0cbad70441cb796f7a9e/aiohttp-3.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdad66685fcf2ad14ce522cf849d4a025f4fd206d6cfc3f403d9873e4c243b03", size = 1660460 },
{ url = "https://files.pythonhosted.org/packages/96/be/8c8569b965ea993cca1bf0788c146c2b4902341cdcf2252b946a639c82ba/aiohttp-3.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8b95a63a8e8b5f0464bd8b1b0d59d2bec98a59b6aacc71e9be23df6989b3dfb", size = 1713465 },
{ url = "https://files.pythonhosted.org/packages/f7/05/00656e6ed46de5730d2db43ad54970b757fef6d573de4ab3f8e12e4da63c/aiohttp-3.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7bcfcede95531589295f56e924702cef7f9685c9e4e5407592e04ded6a65bf3", size = 1770117 },
{ url = "https://files.pythonhosted.org/packages/99/f4/bf2afd31461a8cfdd83769602f6ea9d401c2f4860c91e518513517d4adf0/aiohttp-3.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ecc2fb1a0a9d48cf773add34196cddf7e488e48e9596e090849751bf43098f4", size = 1670818 },
{ url = "https://files.pythonhosted.org/packages/b7/c3/02c47e5e41b643687f781ba5e61011579850d7e04f1dc76050df71f62522/aiohttp-3.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fef105113d56e817cb9bcc609667ee461321413a7b972b03f5b4939f40f307c", size = 1598283 },
{ url = "https://files.pythonhosted.org/packages/e6/f0/2fbb9483c4cabbdc87696b8bf67b587481f48667a01ba2727cc9f959519c/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d33b4490026968bdc7f0729b9d87a3a6b1e09043557d2fc1c605c6072deb2f11", size = 1614447 },
{ url = "https://files.pythonhosted.org/packages/a1/74/d9c2ea812bda194647e0072d27cfc9b362b658a44e67508e392b7c91b4bc/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6362f50a6f0e5482c4330d2151cb682779230683da0e155c15ec9fc58cb50b6a", size = 1614934 },
{ url = "https://files.pythonhosted.org/packages/d6/05/3b8379f464921a8eb24fe58e7518c746c55811758efc5862b2700caf5337/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f698aa61879df64425191d41213dfd99efdc1627e6398e6d7aa5c312fac9702", size = 1682139 },
{ url = "https://files.pythonhosted.org/packages/10/ea/e5e46efb8bd1ba8f50630848b563bb470a0f70b0dcd89b1331a0b1760887/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0e7a0762cc29cd3acd01a4d2b547b3af7956ad230ebb80b529a8e4f3e4740fe8", size = 1712695 },
{ url = "https://files.pythonhosted.org/packages/2f/66/a263ffeaeca2933156334d1d4f4f7079d07b47d47d739ed95f3dc507311a/aiohttp-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e4fb7f5354d39490d8209aefdf5830b208d01c7293a2164e404312c3d8bc55", size = 1667638 },
{ url = "https://files.pythonhosted.org/packages/c0/43/8db1b8402b96510e864ad9595cc84d3d990c1f6e3407f00deb5f6d078f5d/aiohttp-3.11.0-cp313-cp313-win32.whl", hash = "sha256:6c5a6958f4366496004cf503d847093d464814543f157ef3b738bbf604232415", size = 407937 },
{ url = "https://files.pythonhosted.org/packages/40/7f/c548fc3b3069e3b5c3e3da5c559293be75d5b1cff1dd9eb33aefcad1ec4a/aiohttp-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:3ed360d6672a9423aad39902a4e9fe305464d20ed7931dbdba30a4625782d875", size = 433946 },
]
[[package]]
name = "aiosignal"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "frozenlist" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "attrs"
version = "24.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 },
]
[[package]]
name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
]
[[package]]
name = "cryptography"
version = "43.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 },
{ url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 },
{ url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 },
{ url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 },
{ url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 },
{ url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 },
{ url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 },
{ url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 },
{ url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 },
{ url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 },
{ url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 },
{ url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 },
{ url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 },
{ url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 },
{ url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 },
{ url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 },
{ url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 },
{ url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 },
]
[[package]]
name = "frozenlist"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 },
{ url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 },
{ url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 },
{ url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 },
{ url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 },
{ url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 },
{ url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 },
{ url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 },
{ url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 },
{ url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 },
{ url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 },
{ url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 },
{ url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 },
{ url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 },
{ url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 },
{ url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 },
{ url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 },
{ url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 },
{ url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 },
{ url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 },
{ url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 },
{ url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 },
{ url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 },
{ url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 },
{ url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 },
{ url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 },
{ url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 },
{ url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 },
{ url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 },
{ url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 },
{ url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 },
]
[[package]]
name = "greenlet"
version = "3.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 },
{ url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 },
{ url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 },
{ url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 },
{ url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 },
{ url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 },
{ url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 },
{ url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 },
{ url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 },
{ url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 },
{ url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 },
{ url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 },
{ url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 },
{ url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 },
{ url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 },
{ url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 },
{ url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 },
{ url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 },
{ url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 },
{ url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 },
{ url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 },
{ url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 },
{ url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 },
{ url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 },
{ url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "multidict"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 },
{ url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 },
{ url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 },
{ url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 },
{ url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 },
{ url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 },
{ url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 },
{ url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 },
{ url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 },
{ url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 },
{ url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 },
{ url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 },
{ url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 },
{ url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 },
{ url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 },
{ url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 },
{ url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 },
{ url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 },
{ url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 },
{ url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 },
{ url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 },
{ url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 },
{ url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 },
{ url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 },
{ url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 },
{ url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 },
{ url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 },
{ url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 },
{ url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 },
{ url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 },
{ url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 },
]
[[package]]
name = "nextcord"
version = "2.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f2/0e/347795cec6bc841ed426bc5e112380335869e9ba0922c5ed7ec7bb8ab1a5/nextcord-2.6.0.tar.gz", hash = "sha256:ccf461157be682cbceaa474e8df4afa302351b8d6ced632f9ccb7aa647b092e7", size = 932913 }
[[package]]
name = "numpy"
version = "2.1.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658 },
{ url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258 },
{ url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249 },
{ url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704 },
{ url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089 },
{ url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185 },
{ url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751 },
{ url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705 },
{ url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077 },
{ url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858 },
{ url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263 },
{ url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771 },
{ url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805 },
{ url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380 },
{ url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451 },
{ url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822 },
{ url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822 },
{ url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598 },
{ url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021 },
{ url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405 },
{ url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062 },
{ url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839 },
{ url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031 },
{ url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977 },
{ url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951 },
{ url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655 },
{ url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902 },
{ url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180 },
{ url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907 },
{ url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 },
]
[[package]]
name = "pandas"
version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 },
{ url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 },
{ url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 },
{ url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 },
{ url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 },
{ url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 },
{ url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 },
{ url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 },
{ url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 },
{ url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 },
{ url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 },
{ url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 },
{ url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 },
{ url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 },
{ url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 },
{ url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 },
{ url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 },
{ url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 },
{ url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 },
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 },
]
[[package]]
name = "propcache"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/46/a41ca1097769fc548fc9216ec4c1471b772cc39720eb47ed7e38ef0006a9/propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", size = 80800 },
{ url = "https://files.pythonhosted.org/packages/75/4f/93df46aab9cc473498ff56be39b5f6ee1e33529223d7a4d8c0a6101a9ba2/propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", size = 46443 },
{ url = "https://files.pythonhosted.org/packages/0b/17/308acc6aee65d0f9a8375e36c4807ac6605d1f38074b1581bd4042b9fb37/propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", size = 45676 },
{ url = "https://files.pythonhosted.org/packages/65/44/626599d2854d6c1d4530b9a05e7ff2ee22b790358334b475ed7c89f7d625/propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", size = 246191 },
{ url = "https://files.pythonhosted.org/packages/f2/df/5d996d7cb18df076debae7d76ac3da085c0575a9f2be6b1f707fe227b54c/propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", size = 251791 },
{ url = "https://files.pythonhosted.org/packages/2e/6d/9f91e5dde8b1f662f6dd4dff36098ed22a1ef4e08e1316f05f4758f1576c/propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", size = 253434 },
{ url = "https://files.pythonhosted.org/packages/3c/e9/1b54b7e26f50b3e0497cd13d3483d781d284452c2c50dd2a615a92a087a3/propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", size = 248150 },
{ url = "https://files.pythonhosted.org/packages/a7/ef/a35bf191c8038fe3ce9a414b907371c81d102384eda5dbafe6f4dce0cf9b/propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", size = 233568 },
{ url = "https://files.pythonhosted.org/packages/97/d9/d00bb9277a9165a5e6d60f2142cd1a38a750045c9c12e47ae087f686d781/propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", size = 229874 },
{ url = "https://files.pythonhosted.org/packages/8e/78/c123cf22469bdc4b18efb78893e69c70a8b16de88e6160b69ca6bdd88b5d/propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", size = 225857 },
{ url = "https://files.pythonhosted.org/packages/31/1b/fd6b2f1f36d028820d35475be78859d8c89c8f091ad30e377ac49fd66359/propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", size = 227604 },
{ url = "https://files.pythonhosted.org/packages/99/36/b07be976edf77a07233ba712e53262937625af02154353171716894a86a6/propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", size = 238430 },
{ url = "https://files.pythonhosted.org/packages/0d/64/5822f496c9010e3966e934a011ac08cac8734561842bc7c1f65586e0683c/propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", size = 244814 },
{ url = "https://files.pythonhosted.org/packages/fd/bd/8657918a35d50b18a9e4d78a5df7b6c82a637a311ab20851eef4326305c1/propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", size = 235922 },
{ url = "https://files.pythonhosted.org/packages/a8/6f/ec0095e1647b4727db945213a9f395b1103c442ef65e54c62e92a72a3f75/propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", size = 40177 },
{ url = "https://files.pythonhosted.org/packages/20/a2/bd0896fdc4f4c1db46d9bc361c8c79a9bf08ccc08ba054a98e38e7ba1557/propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", size = 44446 },
{ url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 },
{ url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 },
{ url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 },
{ url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 },
{ url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 },
{ url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 },
{ url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 },
{ url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 },
{ url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 },
{ url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 },
{ url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 },
{ url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 },
{ url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 },
{ url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 },
{ url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 },
{ url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 },
{ url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 },
]
[[package]]
name = "pycparser"
version = "2.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
]
[[package]]
name = "pymysql"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972 },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "pytz"
version = "2024.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 },
]
[[package]]
name = "ruff"
version = "0.7.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/06/09d1276df977eece383d0ed66052fc24ec4550a61f8fbc0a11200e690496/ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313", size = 3243664 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/56/933d433c2489e4642487b835f53dd9ff015fb3d8fa459b09bb2ce42d7c4b/ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344", size = 10372090 },
{ url = "https://files.pythonhosted.org/packages/20/ea/1f0a22a6bcdd3fc26c73f63a025d05bd565901b729d56bcb093c722a6c4c/ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0", size = 10190037 },
{ url = "https://files.pythonhosted.org/packages/16/74/aca75666e0d481fe394e76a8647c44ea919087748024924baa1a17371e3e/ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9", size = 9811998 },
{ url = "https://files.pythonhosted.org/packages/20/a1/cf446a0d7f78ea1f0bd2b9171c11dfe746585c0c4a734b25966121eb4f5d/ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5", size = 10620626 },
{ url = "https://files.pythonhosted.org/packages/cd/c1/82b27d09286ae855f5d03b1ad37cf243f21eb0081732d4d7b0d658d439cb/ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299", size = 10177598 },
{ url = "https://files.pythonhosted.org/packages/b9/42/c0acac22753bf74013d035a5ef6c5c4c40ad4d6686bfb3fda7c6f37d9b37/ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e", size = 11171963 },
{ url = "https://files.pythonhosted.org/packages/43/18/bb0befb7fb9121dd9009e6a72eb98e24f1bacb07c6f3ecb55f032ba98aed/ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29", size = 11856157 },
{ url = "https://files.pythonhosted.org/packages/5e/91/04e98d7d6e32eca9d1372be595f9abc7b7f048795e32eb2edbd8794d50bd/ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5", size = 11440331 },
{ url = "https://files.pythonhosted.org/packages/f5/dc/3fe99f2ce10b76d389041a1b9f99e7066332e479435d4bebcceea16caff5/ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67", size = 12725354 },
{ url = "https://files.pythonhosted.org/packages/43/7b/1daa712de1c5bc6cbbf9fa60e9c41cc48cda962dc6d2c4f2a224d2c3007e/ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2", size = 11010091 },
{ url = "https://files.pythonhosted.org/packages/b6/db/1227a903587432eb569e57a95b15a4f191a71fe315cde4c0312df7bc85da/ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d", size = 10610687 },
{ url = "https://files.pythonhosted.org/packages/db/e2/dc41ee90c3085aadad4da614d310d834f641aaafddf3dfbba08210c616ce/ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2", size = 10254843 },
{ url = "https://files.pythonhosted.org/packages/6f/09/5f6cac1c91542bc5bd33d40b4c13b637bf64d7bb29e091dadb01b62527fe/ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2", size = 10730962 },
{ url = "https://files.pythonhosted.org/packages/d3/42/89a4b9a24ef7d00269e24086c417a006f9a3ffeac2c80f2629eb5ce140ee/ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16", size = 11101907 },
{ url = "https://files.pythonhosted.org/packages/b0/5c/efdb4777686683a8edce94ffd812783bddcd3d2454d38c5ac193fef7c500/ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc", size = 8611095 },
{ url = "https://files.pythonhosted.org/packages/bb/b8/28fbc6a4efa50178f973972d1c84b2d0a33cdc731588522ab751ac3da2f5/ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088", size = 9418283 },
{ url = "https://files.pythonhosted.org/packages/3f/77/b587cba6febd5e2003374f37eb89633f79f161e71084f94057c8653b7fb3/ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c", size = 8725228 },
]
[[package]]
name = "setuptools"
version = "75.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/db/722a42ffdc226e950c4757b3da7b56ff5c090bb265dccd707f7b8a3c6fee/setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef", size = 1336032 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/df/88ccbee85aefbca071db004fdc8f8d2507d55d5a9dc27ebb93c92edb1bd8/setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829", size = 1222710 },
]
[[package]]
name = "six"
version = "1.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
]
[[package]]
name = "sqlalchemy"
version = "2.0.36"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64')" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/65/9cbc9c4c3287bed2499e05033e207473504dc4df999ce49385fb1f8b058a/sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", size = 9574485 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/bf/005dc47f0e57556e14512d5542f3f183b94fde46e15ff1588ec58ca89555/SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", size = 2092378 },
{ url = "https://files.pythonhosted.org/packages/94/65/f109d5720779a08e6e324ec89a744f5f92c48bd8005edc814bf72fbb24e5/SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", size = 2082778 },
{ url = "https://files.pythonhosted.org/packages/60/f6/d9aa8c49c44f9b8c9b9dada1f12fa78df3d4c42aa2de437164b83ee1123c/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53", size = 3232191 },
{ url = "https://files.pythonhosted.org/packages/8a/ab/81d4514527c068670cb1d7ab62a81a185df53a7c379bd2a5636e83d09ede/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", size = 3243044 },
{ url = "https://files.pythonhosted.org/packages/35/b4/f87c014ecf5167dc669199cafdb20a7358ff4b1d49ce3622cc48571f811c/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", size = 3178511 },
{ url = "https://files.pythonhosted.org/packages/ea/09/badfc9293bc3ccba6ede05e5f2b44a760aa47d84da1fc5a326e963e3d4d9/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", size = 3205147 },
{ url = "https://files.pythonhosted.org/packages/c8/60/70e681de02a13c4b27979b7b78da3058c49bacc9858c89ba672e030f03f2/SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", size = 2062709 },
{ url = "https://files.pythonhosted.org/packages/b7/ed/f6cd9395e41bfe47dd253d74d2dfc3cab34980d4e20c8878cb1117306085/SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", size = 2088433 },
{ url = "https://files.pythonhosted.org/packages/78/5c/236398ae3678b3237726819b484f15f5c038a9549da01703a771f05a00d6/SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", size = 2087651 },
{ url = "https://files.pythonhosted.org/packages/a8/14/55c47420c0d23fb67a35af8be4719199b81c59f3084c28d131a7767b0b0b/SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", size = 2078132 },
{ url = "https://files.pythonhosted.org/packages/3d/97/1e843b36abff8c4a7aa2e37f9bea364f90d021754c2de94d792c2d91405b/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", size = 3164559 },
{ url = "https://files.pythonhosted.org/packages/7b/c5/07f18a897b997f6d6b234fab2bf31dccf66d5d16a79fe329aefc95cd7461/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", size = 3177897 },
{ url = "https://files.pythonhosted.org/packages/b3/cd/e16f3cbefd82b5c40b33732da634ec67a5f33b587744c7ab41699789d492/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", size = 3111289 },
{ url = "https://files.pythonhosted.org/packages/15/85/5b8a3b0bc29c9928aa62b5c91fcc8335f57c1de0a6343873b5f372e3672b/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", size = 3139491 },
{ url = "https://files.pythonhosted.org/packages/a1/95/81babb6089938680dfe2cd3f88cd3fd39cccd1543b7cb603b21ad881bff1/SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", size = 2060439 },
{ url = "https://files.pythonhosted.org/packages/c1/ce/5f7428df55660d6879d0522adc73a3364970b5ef33ec17fa125c5dbcac1d/SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", size = 2084574 },
{ url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 },
]
[[package]]
name = "sqlmodel"
version = "0.0.22"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "sqlalchemy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b5/39/8641040ab0d5e1d8a1c2325ae89a01ae659fc96c61a43d158fb71c9a0bf0/sqlmodel-0.0.22.tar.gz", hash = "sha256:7d37c882a30c43464d143e35e9ecaf945d88035e20117bf5ec2834a23cbe505e", size = 116392 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/b1/3af5104b716c420e40a6ea1b09886cae3a1b9f4538343875f637755cae5b/sqlmodel-0.0.22-py3-none-any.whl", hash = "sha256:a1ed13e28a1f4057cbf4ff6cdb4fc09e85702621d3259ba17b3c230bfb2f941b", size = 28276 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "tzdata"
version = "2024.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/34/943888654477a574a86a98e9896bae89c7aa15078ec29f490fef2f1e5384/tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", size = 193282 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 },
]
[[package]]
name = "yarl"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/54/9c/9c0a9bfa683fc1be7fdcd9687635151544d992cccd48892dc5e0a5885a29/yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47", size = 178163 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/af/e25615c7920396219b943b9ff8b34636ae3e1ad30777649371317d7f05f8/yarl-1.17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61", size = 141839 },
{ url = "https://files.pythonhosted.org/packages/83/5e/363d9de3495c7c66592523f05d21576a811015579e0c87dd38c7b5788afd/yarl-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d", size = 94125 },
{ url = "https://files.pythonhosted.org/packages/e3/a2/b65447626227ebe36f18f63ac551790068bf42c69bb22dfa3ae986170728/yarl-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139", size = 92048 },
{ url = "https://files.pythonhosted.org/packages/a1/f5/2ef86458446f85cde10582054fd5113495ef8ce8477da35aaaf26d2970ef/yarl-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5", size = 331472 },
{ url = "https://files.pythonhosted.org/packages/f3/6b/1ba79758ba352cdf2ad4c20cab1b982dd369aa595bb0d7601fc89bf82bee/yarl-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac", size = 341260 },
{ url = "https://files.pythonhosted.org/packages/2d/41/4e07c2afca3f9ed3da5b0e38d43d0280d9b624a3d5c478c425e5ce17775c/yarl-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463", size = 340882 },
{ url = "https://files.pythonhosted.org/packages/c3/c0/cd8e94618983c1b811af082e1a7ad7764edb3a6af2bc6b468e0e686238ba/yarl-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147", size = 336648 },
{ url = "https://files.pythonhosted.org/packages/ac/fc/73ec4340d391ffbb8f34eb4c55429784ec9f5bd37973ce86d52d67135418/yarl-1.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7", size = 325019 },
{ url = "https://files.pythonhosted.org/packages/57/48/da3ebf418fc239d0a156b3bdec6b17a5446f8d2dea752299c6e47b143a85/yarl-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685", size = 342841 },
{ url = "https://files.pythonhosted.org/packages/5d/79/107272745a470a8167924e353a5312eb52b5a9bb58e22686adc46c94f7ec/yarl-1.17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172", size = 341433 },
{ url = "https://files.pythonhosted.org/packages/30/9c/6459668b3b8dcc11cd061fc53e12737e740fb6b1575b49c84cbffb387b3a/yarl-1.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7", size = 344927 },
{ url = "https://files.pythonhosted.org/packages/c5/0b/93a17ed733aca8164fc3a01cb7d47b3f08854ce4f957cce67a6afdb388a0/yarl-1.17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da", size = 355732 },
{ url = "https://files.pythonhosted.org/packages/9a/63/ead2ed6aec3c59397e135cadc66572330325a0c24cd353cd5c94f5e63463/yarl-1.17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c", size = 362123 },
{ url = "https://files.pythonhosted.org/packages/89/bf/f6b75b4c2fcf0e7bb56edc0ed74e33f37fac45dc40e5a52a3be66b02587a/yarl-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199", size = 356355 },
{ url = "https://files.pythonhosted.org/packages/45/1f/50a0257cd07eef65c8c65ad6a21f5fb230012d659e021aeb6ac8a7897bf6/yarl-1.17.1-cp312-cp312-win32.whl", hash = "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96", size = 83279 },
{ url = "https://files.pythonhosted.org/packages/bc/82/fafb2c1268d63d54ec08b3a254fbe51f4ef098211501df646026717abee3/yarl-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df", size = 89590 },
{ url = "https://files.pythonhosted.org/packages/06/1e/5a93e3743c20eefbc68bd89334d9c9f04f3f2334380f7bbf5e950f29511b/yarl-1.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", size = 139974 },
{ url = "https://files.pythonhosted.org/packages/a1/be/4e0f6919013c7c5eaea5c31811c551ccd599d2fc80aa3dd6962f1bbdcddd/yarl-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", size = 93364 },
{ url = "https://files.pythonhosted.org/packages/73/f0/650f994bc491d0cb85df8bb45392780b90eab1e175f103a5edc61445ff67/yarl-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", size = 91177 },
{ url = "https://files.pythonhosted.org/packages/f3/e8/9945ed555d14b43ede3ae8b1bd73e31068a694cad2b9d3cad0a28486c2eb/yarl-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", size = 333086 },
{ url = "https://files.pythonhosted.org/packages/a6/c0/7d167e48e14d26639ca066825af8da7df1d2fcdba827e3fd6341aaf22a3b/yarl-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", size = 343661 },
{ url = "https://files.pythonhosted.org/packages/fa/81/80a266517531d4e3553aecd141800dbf48d02e23ebd52909e63598a80134/yarl-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", size = 345196 },
{ url = "https://files.pythonhosted.org/packages/b0/77/6adc482ba7f2dc6c0d9b3b492e7cd100edfac4cfc3849c7ffa26fd7beb1a/yarl-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", size = 338743 },
{ url = "https://files.pythonhosted.org/packages/6d/cc/f0c4c0b92ff3ada517ffde2b127406c001504b225692216d969879ada89a/yarl-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", size = 326719 },
{ url = "https://files.pythonhosted.org/packages/18/3b/7bfc80d3376b5fa162189993a87a5a6a58057f88315bd0ea00610055b57a/yarl-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", size = 345826 },
{ url = "https://files.pythonhosted.org/packages/2e/66/cf0b0338107a5c370205c1a572432af08f36ca12ecce127f5b558398b4fd/yarl-1.17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", size = 340335 },
{ url = "https://files.pythonhosted.org/packages/2f/52/b084b0eec0fd4d2490e1d33ace3320fad704c5f1f3deaa709f929d2d87fc/yarl-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", size = 345301 },
{ url = "https://files.pythonhosted.org/packages/ef/38/9e2036d948efd3bafcdb4976cb212166fded76615f0dfc6c1492c4ce4784/yarl-1.17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", size = 354205 },
{ url = "https://files.pythonhosted.org/packages/81/c1/13dfe1e70b86811733316221c696580725ceb1c46d4e4db852807e134310/yarl-1.17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", size = 360501 },
{ url = "https://files.pythonhosted.org/packages/91/87/756e05c74cd8bf9e71537df4a2cae7e8211a9ebe0d2350a3e26949e1e41c/yarl-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", size = 359452 },
{ url = "https://files.pythonhosted.org/packages/06/b2/b2bb09c1e6d59e1c9b1b36a86caa473e22c3dbf26d1032c030e9bfb554dc/yarl-1.17.1-cp313-cp313-win32.whl", hash = "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", size = 308904 },
{ url = "https://files.pythonhosted.org/packages/f3/27/f084d9a5668853c1f3b246620269b14ee871ef3c3cc4f3a1dd53645b68ec/yarl-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", size = 314637 },
{ url = "https://files.pythonhosted.org/packages/52/ad/1fe7ff5f3e8869d4c5070f47b96bac2b4d15e67c100a8278d8e7876329fc/yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", size = 44352 },
]