Compare commits

..
156 Commits
Author SHA1 Message Date
renovate-bot c2a46d7eb8 Update python Docker tag to v3.14.6 2026-06-14 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
45 changed files with 1380 additions and 677 deletions
+3
View File
@@ -1,2 +1,5 @@
.*
Dockerfile
LICENSE
README.md
renovate.json
+9 -3
View File
@@ -2,7 +2,11 @@ name: build_develop
on:
push:
branches: [main]
branches:
- "*"
- "!main"
paths-ignore:
- "README.md"
jobs:
build:
@@ -20,7 +24,9 @@ jobs:
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:develop .
run: docker build -t gitea.cheezers.org/imcompiling/agon:${{env.GIT_BRANCH}} .
- name: Push Docker Image
run: docker push gitea.cheezers.org/imcompiling/agon:develop
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
+1 -3
View File
@@ -23,8 +23,6 @@ jobs:
- 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:latest -t gitea.cheezers.org/imcompiling/agon:${{env.GIT_TAG}} .
- name: Push Latest Image
run: docker push gitea.cheezers.org/imcompiling/agon:latest
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}}
+1
View File
@@ -135,3 +135,4 @@ deleted_backups/
highscores_data/
TODO.txt
sql_tester.py
/database
+2 -2
View File
@@ -8,8 +8,8 @@
"name": "Python: Module",
"type": "debugpy",
"request": "launch",
"module": "main",
"module": "agon",
"justMyCode": true
}
]
}
}
+8 -2
View File
@@ -1,10 +1,16 @@
{
"cSpell.words": [
"dotenv",
"gmtime",
"highscore",
"highscores"
"highscores",
"imcompiling",
"iterrows",
"nextcord",
"ondelete"
],
"python.analysis.diagnosticSeverityOverrides": {
"reportOptionalMemberAccess": false,
"reportOptionalMemberAccess": "warning",
"reportArgumentType": "information",
"reportAttributeAccessIssue": "warning"
}
+5 -5
View File
@@ -1,15 +1,15 @@
FROM python:3.12.3-slim
FROM python:3.14.6-slim
# install uv
COPY --from=ghcr.io/astral-sh/uv:0.6.2 /uv /uvx /bin/
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 . /agon
ADD . /home
# Sync the project into a new environment, using the frozen lockfile
WORKDIR /agon
WORKDIR /home
RUN uv sync --frozen --no-dev
CMD ["uv", "run", "./main.py"]
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)
@@ -2,14 +2,15 @@
from nextcord.ext import commands
from .create import CreateCog
from .delete import DeleteCog
from .help import HelpCog
from .refresh import RefreshCog
from .set import SetCog
from .submission import SubmissionCog
from .update import UpdateCog
from .view import ViewCog
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.
@@ -18,6 +19,7 @@ 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())
@@ -4,9 +4,11 @@ import nextcord
from nextcord.ext import commands
from sqlalchemy import exc, select
from sqlalchemy.orm import Session
from sqlmodel import col
from database_setup.sql_data_classes import Category, CategoryGroup, engine
from utilities.utils import MAX_LINE_LENGTH, autofill_groups, send_highscore_message
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"):
@@ -52,30 +54,30 @@ class CreateCog(commands.Cog, name="Create Cog"):
size (int): How many sores to show in the category message. Is SlashOption.
"""
new_category = Category(
CategoryName=category_name,
GroupId=int(group_id),
Size=size,
IsAscending=is_ascending,
IsTimeRecord=is_time_record,
IsValidated=is_validated,
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(CategoryGroup.GroupId == int(group_id))
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.GroupName}"!', ephemeral=True
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.GroupName}". Please rename.', ephemeral=True
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.")
@@ -97,10 +99,9 @@ class CreateCog(commands.Cog, name="Create Cog"):
min_value=1,
max_value=30,
),
channel: nextcord.abc.GuildChannel | None = nextcord.SlashOption(
channel: nextcord.TextChannel | nextcord.Thread | None = nextcord.SlashOption(
description="Channel to post Group in. If left empty, defaults to current channel.",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
@@ -119,14 +120,16 @@ class CreateCog(commands.Cog, name="Create Cog"):
"""
# if no channel provided, use current channel.
if channel is None:
channel = interaction.channel
channel_id: int = interaction.channel.id
else:
channel_id: int = channel.id
new_group = CategoryGroup(
GroupName=group_name,
ChannelId=channel.id,
GuildId=interaction.guild_id,
PadLength=pad_length,
UsernameLength=username_length,
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:
@@ -4,10 +4,11 @@ import nextcord
from nextcord.ext import commands
from sqlalchemy import distinct, select
from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, Score, engine
from utilities.utils import (
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,
@@ -36,10 +37,9 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
async def channel_messages(
self,
interaction: nextcord.Interaction,
channel: nextcord.abc.GuildChannel | None = nextcord.SlashOption(
channel: nextcord.TextChannel | nextcord.Thread | None = nextcord.SlashOption(
description="Channel to clear of messages.",
required=False,
channel_types=[nextcord.ChannelType.text],
default=None,
),
) -> None:
@@ -54,13 +54,18 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
Is a SlashOption. Limited to text channels. Defaults to None.
"""
if not channel:
channel = interaction.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(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.ChannelId == channel.id)
.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)
@@ -68,12 +73,13 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
await interaction.response.defer(ephemeral=True)
for group in groups:
try:
message: nextcord.Message = await channel.fetch_message(group.MessageId)
await message.delete()
except nextcord.NotFound:
# silently ignore that messages are not found. They got deleted already.
pass
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)
@@ -97,24 +103,24 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
user_name (str): User to remove from highscores
"""
with Session(engine) as session:
stmt = select(distinct(Score.CategoryId)).where(Score.UserName == user_name)
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(Score.UserName == user_name)
stmt = select(Score).where(col(Score.username) == user_name)
for score in session.scalars(stmt):
session.delete(score)
session.commit()
group_ids = select(Category.GroupId).where(Category.CategoryId.in_(category_ids))
group_ids = select(col(Category.group_id)).where(col(Category.id).in_(category_ids))
stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_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 perminant.")
@delete.subcommand(description="Delete a category from a group. Deletion is permanent.")
async def category(
self,
interaction: nextcord.Interaction,
@@ -133,7 +139,7 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
DeleteConfirmView(custom_id=create_delete_id(int(group_id), int(category_id)))
)
@delete.subcommand(description="Delete a group. Deletion is perminant.")
@delete.subcommand(description="Delete a group. Deletion is permanent.")
async def group(
self,
interaction: nextcord.Interaction,
@@ -172,13 +178,15 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
user_name (str): The user to remove. Is SlashOption.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt)
stmt = select(Category.CategoryName).where(Category.CategoryId == int(category_id))
stmt = select(col(Category.name)).where(col(Category.id) == int(category_id))
category_name = session.scalar(stmt)
stmt = select(Score).where(Score.CategoryId == int(category_id)).where(Score.UserName == user_name)
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))
@@ -205,10 +213,10 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
user_name (str): The user to remove. Is SlashOption.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt)
category_ids = select(Category.CategoryId).where(Category.GroupId == int(group_id))
stmt = select(Score).where(Score.CategoryId.in_(category_ids)).where(Score.UserName == user_name)
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)
@@ -216,7 +224,7 @@ class DeleteCog(commands.Cog, name="Delete Cog"):
await send_highscore_message(session, interaction.guild, group)
await interaction.response.send_message(
f'User "{user_name}" removed from all categories in {group.GroupName}.', ephemeral=True
f'User "{user_name}" removed from all categories in {group.name}.', ephemeral=True
)
@from_category.on_autocomplete("group_id")
+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))
@@ -11,7 +11,7 @@ class HelpCog(commands.Cog, name="Help commands."):
"""Default constructor for HelpCog class. Does nothing."""
@nextcord.slash_command()
async def help(self, interaction: nextcord.Interaction):
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.")
@@ -1,15 +1,14 @@
"""
Commands for refreshing highscore messages.
"""
"""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
# pylint: disable=import-error
from database_setup.sql_data_classes import CategoryGroup, engine
from utilities.utils import autofill_groups, send_highscore_message
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"):
@@ -44,13 +43,13 @@ class RefreshCog(commands.Cog, name="Refresh Cog"):
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
.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.GroupName}!", ephemeral=True)
await interaction.response.send_message(f"Updated group {group.name}!", ephemeral=True)
@refresh.subcommand()
async def all(self, interaction: nextcord.Interaction) -> None:
@@ -68,7 +67,7 @@ class RefreshCog(commands.Cog, name="Refresh Cog"):
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == interaction.guild_id)
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)
+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))
@@ -6,11 +6,13 @@ import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
from cogs.submission.verification_view import VerificationView
from database_setup.sql_data_classes import Category, CategoryGroup, Guild, Submission, engine
from main import logger
from utilities.utils import autofill_categories, autofill_groups, submit_score
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):
@@ -19,8 +21,10 @@ class SubmissionCog(commands.Cog):
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: PLR0913
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."),
@@ -56,46 +60,50 @@ class SubmissionCog(commands.Cog):
with Session(engine) as session:
stmt = (
select(Category).where(Category.GroupId == int(group_id)).where(Category.CategoryId == int(category_id))
select(Category)
.where(col(Category.group_id) == int(group_id))
.where(col(Category.id) == int(category_id))
)
category: Category = session.scalar(stmt)
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.IsValidated and proof is None:
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
verified_score = await self.verify_score(interaction, score, is_time_record=category.IsTimeRecord)
# check if verify_score was successful. It replies with corrections required itself.
if verified_score is None:
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.IsValidated:
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(Guild).where(Guild.GuildId == interaction.guild_id)
verification_channel_id: int = session.scalar(stmt).VerificationChannel
stmt = select(col(Guild.verification_channel)).where(col(Guild.id) == interaction.guild_id)
channel = interaction.guild.get_channel(verification_channel_id)
if channel is None:
error_response = (
"Registered Verification channel does not exist or was never registered. Contact server admins."
)
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 not isinstance(channel, nextcord.TextChannel):
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.")
logger.error(
agon_logger.error(
"Channel id %s in guild %s is does not correspond to a TextChannel.", channel.id, channel.guild.id
)
return
@@ -111,13 +119,13 @@ class SubmissionCog(commands.Cog):
# Save the submission data for later use.
submission: Submission = Submission(
CategoryId=category_id,
GroupId=group_id,
GuildId=interaction.guild_id,
MessageId=verification_message.id,
Score=verified_score,
UserId=interaction.user.id,
UserName=username,
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)
@@ -151,15 +159,15 @@ class SubmissionCog(commands.Cog):
# Get group name from database
stmt = (
select(CategoryGroup.GroupName)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == group_id)
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(Category.CategoryName).where(Category.GroupId == group_id).where(Category.CategoryId == category_id)
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)
@@ -200,9 +208,9 @@ class SubmissionCog(commands.Cog):
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):
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")
score = score[1:]
return score
@@ -1,23 +1,20 @@
"""
Commands for updating information/variables held in database.
"""
"""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
# pylint: disable=import-error
from database_setup.sql_data_classes import Guild, engine
from utilities.utils import autofill_users_in_guild
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.
"""
"""Class holds commands and views needed for updating things in bot."""
def __init__(self, bot: commands.Bot):
"""
@@ -29,17 +26,13 @@ class UpdateCog(commands.Cog, name="Update commands."):
self._bot: commands.Bot = bot
@commands.Cog.listener()
async def on_ready(self):
"""
Called when cog is loaded. Adds views required for DataManagement commands.
"""
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):
"""
Parent slash command for updating things. Never called.
"""
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(
@@ -47,10 +40,11 @@ class UpdateCog(commands.Cog, name="Update commands."):
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.
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
@@ -58,9 +52,13 @@ class UpdateCog(commands.Cog, name="Update commands."):
new_username (str): Username to replace old with.
"""
with Session(engine) as session:
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
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)
@@ -72,12 +70,12 @@ class UpdateCog(commands.Cog, name="Update commands."):
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):
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.
old_username (str): String to run autocompletion against.
username (str): String to run autocompletion against.
"""
await interaction.response.send_autocomplete(autofill_users_in_guild(interaction.guild_id, username))
@@ -5,10 +5,11 @@ import pandas as pd
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, Score, engine
from utilities.utils import MAX_LINE_LENGTH, autofill_categories, autofill_groups, format_score_string
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"):
@@ -47,17 +48,17 @@ class ViewCog(commands.Cog, name="View Command"):
# get group, category, and all scores sorted correctly.
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == int(group_id))
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == int(group_id))
group = session.scalar(stmt)
stmt = select(Category).where(Category.CategoryId == int(category_id))
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(Score.CategoryId == category_id).statement, con=engine
sql=session.query(Score).where(col(Score.category_id) == category_id).statement, con=engine
)
scores = d_f.sort_values("Score", ascending=category.IsAscending).reset_index()
scores = d_f.sort_values("Score", ascending=category.is_ascending).reset_index()
# get the longest string
longest_str: int = scores.UserName.map(len).max()
@@ -68,13 +69,13 @@ class ViewCog(commands.Cog, name="View Command"):
row["UserName"],
row["Score"],
longest_str,
category.IsTimeRecord,
category.is_time_record,
i + 1, # type: ignore[reportOperatorIssue]
MAX_LINE_LENGTH,
)
)
message_str: str = f"**{group.GroupName} {category.CategoryName} Leaderboard!**\n```"
message_str: str = f"**{group.name} {category.name} Leaderboard!**\n```"
if len(str_list) == 0:
message_str += "No Submissions!"
else:
+1
View File
@@ -0,0 +1 @@
"""Package for views used in commands."""
@@ -1,23 +1,19 @@
"""
Contains view and helper functions for confirming and processing deletes.
"""
"""Contains view and helper functions for confirming and processing deletes."""
import nextcord
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, engine
from utilities.utils import send_highscore_message
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.
"""
"""View modal popup for confirming deletions."""
def __init__(self, custom_id: str):
super().__init__(title="Warning, deletion is perminant.", custom_id=custom_id)
super().__init__(title="Warning, deletion is permanent.", custom_id=custom_id)
self.checker = nextcord.ui.TextInput(
label='Type "delete" to confirm deletion.',
@@ -27,7 +23,12 @@ class DeleteConfirmView(nextcord.ui.Modal):
)
self.add_item(self.checker)
async def callback(self, interaction: nextcord.Interaction):
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
@@ -35,21 +36,23 @@ class DeleteConfirmView(nextcord.ui.Modal):
del_vals = parse_delete_id(self.custom_id)
with Session(engine) as session:
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == del_vals["group_id"])
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: nextcord.TextChannel = interaction.guild.get_channel(group.ChannelId)
message: nextcord.Message = await channel.fetch_message(group.MessageId)
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.GroupId == del_vals["group_id"])
.where(Category.CategoryId == del_vals["category_id"])
.where(Category.group_id == del_vals["group_id"])
.where(Category.id == del_vals["category_id"])
)
session.execute(stmt)
# update the message
@@ -78,7 +81,7 @@ def parse_delete_id(delete_id: str) -> dict:
Returns a dict containing the values of a delete id.
Args:
id (str): the delete id
delete_id (str): the delete id
Returns:
dict: dict containing values from a delete id.
@@ -3,23 +3,22 @@
import nextcord
from sqlalchemy import distinct, exc, select
from sqlalchemy.orm import Session
from sqlmodel import col
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, Score, engine
from utilities.utils import send_highscore_message
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):
"""
Default constructor for UpdateUsernameVerifyView. Calls super constructor with no timeout.
"""
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): # pylint: disable=unused-argument
async def deny_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Button for denying username update. Deletes message.
@@ -30,10 +29,11 @@ class UpdateUsernameVerifyView(nextcord.ui.View):
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): # pylint: disable=unused-argument
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.
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
@@ -42,20 +42,20 @@ class UpdateUsernameVerifyView(nextcord.ui.View):
with Session(engine) as session:
try:
embed_content = interaction.message.embeds[0].to_dict()
old_username: str = embed_content["fields"][0]["value"]
new_username: str = embed_content["fields"][1]["value"]
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(Score.CategoryId)).where(Score.UserName == old_username)
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(Score.UserName == old_username)
stmt = select(Score).where(col(Score.username) == old_username)
for score in session.scalars(stmt):
score.UserName = new_username
session.commit()
group_ids = select(Category.GroupId).where(Category.CategoryId.in_(category_ids))
group_ids = select(col(Category.group_id)).where(col(Category.id).in_(category_ids))
stmt = select(CategoryGroup).where(CategoryGroup.GroupId.in_(group_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)
@@ -1,14 +1,13 @@
"""
Defines cog for guild listeners. Contains functions for various guild events.
"""
"""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
# pylint: disable=import-error
from database_setup.sql_data_classes import Guild, engine
from agon.database.db import engine
from agon.database.models import Guild
class GuildListener(commands.Cog, name="Guild Listener"):
@@ -18,7 +17,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild):
async def on_guild_join(self, guild: nextcord.Guild) -> None:
"""
Listener for Guild Join. Adds guild to database.
@@ -26,12 +25,11 @@ class GuildListener(commands.Cog, name="Guild Listener"):
guild (nextcord.Guild): Guild that was joined.
"""
with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == guild.id)
guild_data = session.scalar(stmt)
stmt = select(Guild).where(col(Guild.id == guild.id))
# just in case check if the guild already exists
if guild_data is None:
new_guild = Guild(GuildId=guild.id, GuildName=guild.name)
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
@@ -45,7 +43,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
await imcompiling.send(f'Agon has joined server "{guild.name}"!')
@commands.Cog.listener()
async def on_guild_remove(self, guild: nextcord.Guild):
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.
@@ -53,7 +51,7 @@ class GuildListener(commands.Cog, name="Guild Listener"):
guild (nextcord.Guild): Guild that was left.
"""
with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == guild.id)
stmt = select(Guild).where(col(Guild.id == guild.id))
guild_data = session.scalar(stmt)
guild_data.Active = False
session.commit()
@@ -66,6 +64,6 @@ class GuildListener(commands.Cog, name="Guild Listener"):
# 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(GuildListener(bot))
@@ -1,6 +1,4 @@
"""
Initializing code for all loops.
"""
"""Initializing code for all loops."""
from nextcord.ext import commands
@@ -10,7 +8,7 @@ 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):
"""Adds function to bot"""
def setup(bot: commands.Bot) -> None:
"""Adds function to bot."""
bot.add_cog(BotPresence(bot))
bot.add_cog(GuildData(bot))
@@ -1,6 +1,4 @@
"""
Functions for controlling the bot's presence. Also contains the loop.
"""
"""Functions for controlling the bot's presence. Also contains the loop."""
import random
@@ -11,27 +9,26 @@ from nextcord.ext import commands, tasks
class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions."""
def __init__(self, bot: commands.Bot):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild): # pylint: disable=unused-argument
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):
async def on_ready(self) -> None:
"""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
self.periodic_change_presence.start()
@tasks.loop(minutes=2)
async def periodic_change_presence(self):
async def periodic_change_presence(self) -> None:
"""Loops every 2 minutes and changes presence."""
await self.change_presence()
async def change_presence(self):
async def change_presence(self) -> None:
"""Randomly chooses a presence and updates bot's presence."""
statuses = [
"Now with slash commands!",
@@ -41,7 +38,7 @@ class BotPresence(commands.Cog, name="Bot Presence"):
"That's a lot of damage!",
]
status = random.choice(statuses)
status = random.choice(statuses) # noqa: S311
status += f" | {len(self.bot.guilds)} servers"
@@ -1,48 +1,44 @@
"""
Contains a loop that updates guild data in the database periodically.
"""
"""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
# pylint: disable=import-error
from database_setup.sql_data_classes import Guild, engine
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:
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@commands.Cog.listener()
async def on_ready(self):
async def on_ready(self) -> None:
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_update_guild_data is subclassed from tasks.loop.
await self.periodic_update_guild_data.start() # pylint: disable=no-member
await self.periodic_update_guild_data.start()
@tasks.loop(hours=24)
async def periodic_update_guild_data(self):
async def periodic_update_guild_data(self) -> None:
"""Loops every day, calls update_guild_data."""
self.update_guild_data()
def update_guild_data(self):
"""
Updates all known guild's data in the database.
"""
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(Guild.GuildId == 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(GuildId=guild.id, GuildName=guild.name)
new_guild = Guild(id=guild.id, name=guild.name)
session.add(new_guild)
else:
guild_data.GuildName = guild.name
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))
@@ -3,11 +3,11 @@
import nextcord
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from sqlmodel import col
from database_setup.sql_data_classes import Submission, engine
# pylint: disable=import-error
from utilities.utils import submit_score
from agon.database.db import engine
from agon.database.models import Submission
from agon.utils import submit_score
class VerificationView(nextcord.ui.View):
@@ -22,8 +22,8 @@ class VerificationView(nextcord.ui.View):
with Session(engine) as session:
stmt = (
delete(Submission)
.where(Submission.GuildId == interaction.guild_id)
.where(Submission.MessageId == interaction.message.id)
.where(col(Submission.guild_id) == interaction.guild_id)
.where(col(Submission.message_id) == interaction.message.id)
)
session.execute(stmt)
session.commit()
@@ -35,23 +35,20 @@ class VerificationView(nextcord.ui.View):
with Session(engine) as session:
stmt = (
select(Submission)
.where(Submission.GuildId == interaction.guild_id)
.where(Submission.MessageId == interaction.message.id)
.where(col(Submission.guild_id) == interaction.guild_id)
.where(col(Submission.message_id) == interaction.message.id)
)
submission = session.scalar(stmt)
if submission is None:
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.GroupId, submission.CategoryId, submission.UserName, submission.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}>."
# remove the bot information embed. it should always be the first one.
await interaction.message.edit(content=content, view=None)
+2 -3
View File
@@ -1,6 +1,4 @@
"""
Loads environment variables for use in bot.
"""
"""Loads environment variables for use in bot."""
import os
@@ -15,3 +13,4 @@ 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)
+81 -63
View File
@@ -6,9 +6,11 @@ import nextcord
import pandas as pd
from sqlalchemy import distinct, select
from sqlalchemy.orm import Session, aliased
from sqlmodel import col
# pylint: disable=import-error
from database_setup.sql_data_classes import Category, CategoryGroup, Guild, Score, engine
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
@@ -31,20 +33,19 @@ def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "d
# search database for all category names
subq = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == guild_id)
.where(CategoryGroup.GroupId == group_id)
.where(col(CategoryGroup.guild_id) == guild_id)
.where(col(CategoryGroup.id) == group_id)
.subquery()
)
cat_group_alias = aliased(CategoryGroup, subq)
stmt = select(Category).where(Category.GroupId == cat_group_alias.GroupId)
categories_dict: dict[str, str] = {cat.CategoryName: str(cat.CategoryId) for cat in session.scalars(stmt)}
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
# TODO is it better to have sql handle the starts with or python?
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()
}
@@ -64,10 +65,10 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
"""
with Session(engine) as session:
# Select all groups in guild.
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == guild_id)
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.GroupName: str(group.GroupId) for group in session.scalars(stmt)}
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:
@@ -83,8 +84,10 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
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 unioning all users in group.
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.
@@ -99,27 +102,27 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
# get all categories in the group
subq = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == guild_id)
.where(CategoryGroup.GroupId == group_id)
.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(Category.CategoryId).where(Category.GroupId == category_group.GroupId)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(subq))
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(Category.GroupId == category_group.GroupId)
.where(Category.CategoryId == category_id)
.where(col(Category.group_id) == category_group.id)
.where(col(Category.id) == category_id)
.subquery()
)
category = aliased(Category, subq)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId == category.CategoryId)
stmt = select(distinct(col(Score.username))).where(col(Score.category_id) == category.id)
users_list: list[str] = list(session.scalars(stmt))
@@ -145,9 +148,9 @@ def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
list[str]: All users with submissions in the guild.
"""
with Session(engine) as session:
group_ids = select(CategoryGroup.GroupId).where(CategoryGroup.GuildId == guild_id)
category_ids = select(Category.CategoryId).where(Category.GroupId.in_(group_ids))
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(category_ids))
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
@@ -176,34 +179,34 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
Returns:
str: Formatted string for discord message
"""
ret_string = f"**{group.GroupName}**\n```"
ret_string = f"**{group.name}**\n```"
stmt = select(Guild).where(Guild.GuildId == group.GuildId)
stmt = select(Guild).where(col(Guild.id) == group.guild_id)
# Get all categories in the group
subq = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == group.GuildId)
.where(CategoryGroup.GroupName == group.GroupName)
.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(Category.GroupId == cat_group_alias.GroupId)
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 = []
categories: list[Category] = []
category_ids = []
max_cat_size: int = 0
pad_length = group.PadLength
pad_length = group.pad_length
for cat in session.scalars(stmt):
max_cat_size = max(cat.Size, max_cat_size)
max_cat_size = max(cat.size, max_cat_size)
categories.append(cat)
category_ids.append(cat.CategoryId)
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(Score.CategoryId.in_(category_ids)).statement,
sql=session.query(Score).where(col(Score.category_id).in_(category_ids)).statement,
con=engine,
)
@@ -214,15 +217,15 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
for category in categories:
# category name string
str_list[0] += f"{category.CategoryName}".ljust(pad_length, " ")
str_list[0] += f"{category.name}".ljust(pad_length, " ")
# ensure there is data to print
if category.CategoryId in groups_df.indices:
group_df = groups_df.get_group(category.CategoryId)
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.IsAscending).head(category.Size).reset_index()
group_df.sort_values("Score", ascending=category.is_ascending).head(category.size).reset_index()
)
# add each score to list
@@ -234,19 +237,19 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
str_list[ind + 1] += format_score_string(
username,
score,
group.UsernameLength,
category.IsTimeRecord,
group.username_length,
category.is_time_record,
ind + 1,
pad_length,
)
# if not enough entries, pad out
elif ind + 1 <= category.Size:
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:
if ind + 1 <= category.size:
str_list[ind + 1] += f"{ind + 1}: ".ljust(pad_length, " ")
else:
str_list[ind + 1] += "".ljust(pad_length, " ")
@@ -273,6 +276,8 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
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,
@@ -301,13 +306,13 @@ def format_score_string(
if is_time_record:
# MM:SS.ms is 7 characters long
# if there are single digit minutes, add a space for formatting.
score = get_time_str(score)
if score.find(":") and len(score.split(":", maxsplit=1)[0]) < 2:
score = " " + score
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:
score = str(int(score)).rjust(7, " ")
formatted_score = str(int(score)).rjust(7, " ")
return f"{rank}: {username} - {score}".ljust(pad_length, " ")
return f"{rank}: {username} - {formatted_score}".ljust(pad_length, " ")
def get_time_str(time_in: float) -> str:
@@ -325,32 +330,44 @@ def get_time_str(time_in: float) -> str:
return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(time_in))
async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> nextcord.Message:
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.
Returns:
nextcord.Message: Message object that was posted with group contents.
"""
highscore_string = format_highscore_message(session, group)
channel = guild.get_channel(group.ChannelId)
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 group.MessageId is None:
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.MessageId = message.id
group.message_id = message.id
session.commit()
else:
message: nextcord.Message = await channel.fetch_message(group.MessageId)
await message.edit(content=highscore_string)
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 message
return
async def submit_score(
@@ -362,37 +379,38 @@ async def submit_score(
) -> 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 (CategoryGroup): group to submit score to.
category_id (Category): Category to submit score to.
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(Category.IsTimeRecord).where(Category.CategoryId == category_id)
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 = (minutes * 60) + seconds
score_f = (minutes * 60) + seconds
else:
score = float(score)
score_f = float(score)
stmt = select(Score).where(Score.UserName == username).where(Score.CategoryId == category_id)
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
existing_score.score = score_f
else:
score_obj = Score(UserName=username, Score=score, CategoryId=category_id)
score_obj = Score(username=username, score=score_f, category_id=category_id)
session.add(score_obj)
session.commit()
stmt = select(CategoryGroup).where(CategoryGroup.GroupId == group_id)
stmt = select(CategoryGroup).where(col(CategoryGroup.id) == group_id)
group = session.scalar(stmt)
await send_highscore_message(session, interaction.guild, group)
-207
View File
@@ -1,207 +0,0 @@
"""
Set command heirarchy.
"""
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
# pylint: disable=import-error
from database_setup.sql_data_classes import CategoryGroup, Guild, engine
from utilities.utils import MAX_LINE_LENGTH, autofill_groups
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):
"""
Parent slash command for setting settings. Never called.
"""
@set.subcommand(description="Registers the channel for a Category Group's messages.")
async def group_output_channel(
self,
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(
name="group", description="Category 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 not channel:
channel = interaction.channel
channel_id = channel.id
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.ChannelId = channel_id
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@set.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="Category 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(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.PadLength = length
session.commit()
await interaction.response.send_message(
f"Category pad length set to {length} for {group.GroupName}", ephemeral=True
)
@set.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 Optional[nextcord.abc.GuildChannel], optional): Channel to register
Is a SlashOption. Limited to text channels, Defaults to None.
"""
if not channel:
channel = interaction.channel
with Session(engine) as session:
channel_id = channel.id
stmt = select(Guild).where(Guild.GuildId == interaction.guild_id)
guild = session.scalar(stmt)
guild.SubmissionChannel = channel_id
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@set.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
length (int): Number of characters. Is SlashOption. Defaults to 12.
"""
with Session(engine) as session:
stmt = (
select(CategoryGroup)
.where(CategoryGroup.GuildId == interaction.guild_id)
.where(CategoryGroup.GroupId == int(group_id))
)
group = session.scalar(stmt)
group.UsernameLength = length
session.commit()
await interaction.response.send_message(f"Username length 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.
"""
if not channel:
channel = interaction.channel
channel_id = channel.id
with Session(engine) as session:
stmt = select(Guild).where(Guild.GuildId == interaction.guild_id)
guild = session.scalar(stmt)
guild.VerificationChannel = channel_id
session.commit()
await interaction.send(f"Registered <#{channel_id}>.", ephemeral=True)
@group_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):
"""
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))
-13
View File
@@ -1,13 +0,0 @@
"""Small script to create the database if it doesn't already exist."""
import sqlite3
from pathlib import Path
if not Path("database/dakiny.db").exists():
with sqlite3.connect("database/dakiny.db") as con:
cur = con.cursor()
with Path("database_setup/database.sql", encoding="utf-8").open() as file:
database_script = file.read()
cur.executescript(database_script)
con.commit()
-7
View File
@@ -1,7 +0,0 @@
[tool.ruff]
# Extend the `pyproject.toml` file in the parent directory...
extend = "../pyproject.toml"
[tool.ruff.lint]
# ignore implicit-namespace-package in this directory.
ignore = ["INP001"]
-18
View File
@@ -1,18 +0,0 @@
"""Simple file to set up the sqlalchemy connection to database."""
from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base
from utilities import config
# connect with sql alchemy and autoload schema
url = f"mysql+pymysql://{config.SQL_USER}:{config.SQL_PASS}@{config.SQL_HOST}/{config.SQL_DB}"
engine = create_engine(url)
Base = automap_base()
Base.prepare(autoload_with=engine)
# define the classes
Category = Base.classes.Categories
CategoryGroup = Base.classes.CategoryGroups
Guild = Base.classes.Guilds
Score = Base.classes.Scores
Submission = Base.classes.Submissions
-56
View File
@@ -1,56 +0,0 @@
CREATE DATABASE highscores;
use highscores;
CREATE TABLE Guilds(
GuildId bigint unsigned not null unique,
GuildName varchar(95) not null,
SubmissionChannel bigint unsigned, -- deprecated, is not used anymore.
VerificationChannel bigint unsigned,
Active boolean DEFAULT true,
PRIMARY KEY (GuildId)
);
CREATE TABLE CategoryGroups(
GroupId int unsigned not null unique AUTO_INCREMENT,
GroupName varchar(50) not null,
ChannelId bigint unsigned,
GuildId bigint unsigned not null,
MessageId bigint unsigned,
PadLength smallint not null DEFAULT 26,
UsernameLength smallint DEFAULT 12,
CONSTRAINT UniqueGuildCategoryGroup UNIQUE (GuildId, GroupName),
FOREIGN KEY (GuildId) REFERENCES Guilds(GuildId) ON DELETE CASCADE,
PRIMARY KEY (GroupId)
);
CREATE TABLE Categories(
CategoryId int unsigned not null unique AUTO_INCREMENT,
CategoryName varchar(30) not null,
GroupId int unsigned not null,
Size smallint not null DEFAULT 3,
IsAscending boolean not null DEFAULT true,
IsTimeRecord boolean not null DEFAULT false,
IsValidated boolean not null DEFAULT true,
CONSTRAINT UniqueCategoryInGroup UNIQUE (GroupId, CategoryName),
FOREIGN KEY (GroupId) REFERENCES CategoryGroups(GroupId) ON DELETE CASCADE,
PRIMARY Key (CategoryId)
);
CREATE TABLE Scores(
UserName varchar(30) not null,
Score float not null, -- Maybe should have time and int scores?? timestamp type?
CategoryId int unsigned not null,
FOREIGN KEY (CategoryId) REFERENCES Categories(CategoryId) ON DELETE CASCADE,
PRIMARY KEY (CategoryId, UserName)
);
CREATE TABLE Submissions(
CategoryId int unsigned not null,
GroupId int unsigned not null,
GuildId bigint unsigned not null,
MessageId bigint unsigned not null,
Score varchar(30) not null, -- Score is stored as a string until converted during submission
UserId bigint unsigned not null,
UserName varchar(30) not null,
PRIMARY KEY (CategoryId, UserName) -- one submission a category per user.
);
-58
View File
@@ -1,58 +0,0 @@
"""Main file for Agon bot."""
import logging
import os
import sys
import nextcord
from nextcord.ext import commands
from utilities import config
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)
logger = logging.getLogger("default")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
logger.addHandler(handler)
def main() -> None:
"""Main function for bot."""
min_python_version = (3, 12)
if sys.version_info < min_python_version:
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 os.listdir("cogs"):
bot.load_extension(f"cogs.{folder}")
@bot.listen()
async def on_ready() -> None:
"""When Discord is connected."""
if bot.user is None:
logger.error("Discord connection failed.")
return
logger.info("%s has connected to Discord!", bot.user.name)
# Run Discord bot
bot.run(config.DISCORD_TOKEN)
if __name__ == "__main__":
main()
+1 -1
View File
@@ -11,7 +11,7 @@ dependencies = [
"pymysql>=1.1.1",
"python-dotenv>=1.0.1",
"setuptools>=75.3.0",
"sqlalchemy>=2.0.35",
"sqlmodel>=0.0.22",
]
requires-python = ">=3.12"
Generated
+77 -2
View File
@@ -12,7 +12,7 @@ dependencies = [
{ name = "pymysql" },
{ name = "python-dotenv" },
{ name = "setuptools" },
{ name = "sqlalchemy" },
{ name = "sqlmodel" },
]
[package.dev-dependencies]
@@ -28,7 +28,7 @@ requires-dist = [
{ name = "pymysql", specifier = ">=1.1.1" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "setuptools", specifier = ">=75.3.0" },
{ name = "sqlalchemy", specifier = ">=2.0.35" },
{ name = "sqlmodel", specifier = ">=0.0.22" },
]
[package.metadata.requires-dev]
@@ -102,6 +102,15 @@ 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"
@@ -425,6 +434,59 @@ 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"
@@ -536,6 +598,19 @@ wheels = [
{ 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"